Command Query Separation

امروز میخوایم در مورد یکی دیگه از جنبه های Clean Code صحبت کنیم که بهمون این امکان رو میده تا کدهای readable تر بنویسیم و راحتر بتونیم با کدها ارتباط برقرار کرده و بفهمیمشون.

Command Query Separation Principle اینجاست تا داستان امروزمون رو روایت کنه.

Command Query Separation یا به اختصار CQS (از اینجای مقاله به بعد بهش میگیم CQS) قراره بهمون کمک کنه که فانکشن ها یا متد های کلین تری بنویسیم و نا گفته نماند که این اصل رو از دنیای Functional Programming قرض میگیریم.

قبل از اینکه ببینیم CQS چی میگه, باید ببینیم که Side Effect چی هست. چون که تمام کاری که CQS میکنه, اینه که فانکشن ها رو براساس این که Side Effect دارن یا ندارن سامان دهی یا دسته بندی میکنه.

Side Effect چیه؟

قصد دارم قبل از اینکه با یه جمله ی قلمبه سلمبه بهت بگم Side Effect چی هست و چی نیست, چنتا مثال با هم ببینیم. اینجوری درکش یه نموره راحتر میشه!!!

به مثال زیر نگاه کن:

  • Dart
  • C#

base class Calculator{

  int sum(int a, int b){
    return a + b;
  }
}
public class Calculator
{
    public int Sum(int a, int b)
    {
        return a + b;
    }
}

توی این مثال ساده یه کلاس به اسم Calculator داریم که یه متد به اسم sum داره. این متد دو تا ورودی میگیره (a و b) و حاصل جمع اون ها رو ریترن میکنه.

کاری که این متد داره میکنه, صرفن گرفتن ورودی(input) و انجام یه سری عملیات روی اون (جمع) و تولید یه خروجی (output) هست.

این متد state کلاس یا به عبارتی Instance Field های کلاس رو تغییر نمیده. پس در نتیجه هیچ تاثیری روی دنیای خارج از خودش نمیگذاره. وقتی میگیم که هیچ تاثیری روی دنیای خارج از خودش نمگیذاره, یعنی هر انچه که خارج از متد هست. هر چیزی!!!! پس در نتیجه میگیم که این متد Side Effect نداره.

برای روشن تر شدن موضوع یه مثال دیگه با هم ببینیم:

  • Dart
  • C#

base class User{

  String get name => _name;
  late String _name;
  set name(String value){
    if(value == _name) return;

    _name = value;
  }
}
public class User
{
    private string _name;

    public string Name 
    { 
        get {return _name;}
        set
        {
            if(value == _name) return;

            _name = value;
        }
    }
}

خب یه کلاس User داریم که یه دونه Instance Filed به اسم _name داره. همچنین دو تا متد داریم:

  • set name یا setter
  • get name یا getter

کاری که getter داره انجام میده این هست که داره _name رو ریترن میکنه. پس این متد هیچ چیزی رو خارج از خودش تغییر نمیده و با این حساب میتونیم بگیم که Side Effect نداره.

حالا خوب به setter دقت کن. این متد داره مقدار _name رو تغییر میده. پس داره یه چیزی رو که خارج از خودش هست (یعنی state کلاس رو) تغییر میده. پس میگیم این متد Side Effect داره.

پس تا اینجا میتونیم بگیم که هر موقع متد مورد نظر یه state ای رو تغییر داد, اون متد Side Effect داره. تا اینجا state محدود به کلاس بود, ولی همه چیز محدود به کلاس و تغییر instance field های کلاس نمیشه و در ادامه ی این مقاله موارد دیگه ای از Side Effect رو هم مثال میزنیم.

پایه ای یه مثال دیگه از همین مدل ببینیم؟

  • Dart
  • C#

base class Stack<T>{

  final List<T?> _elements;
  int _size = 0;

  Stack(int capacity) :
    _elements = List.filled(capacity, null);

  bool get isEmpty => _size == 0;
  int get size => _size;

  void push(T element){
    if(_size == _elements.length){
      throw OverFlow();
    }

    _elements[_size++] = element;
  }

  T pop(){
    if(_size == 0){
      throw UnderFlow();
    }

    return _elements[_size--]!;
  }
  
}

base class UnderFlow implements Exception{}
base class OverFlow implements Exception{}
public class Stack<T>
{
    private T[] _elements;
    private int _size = 0;

    public Stack(int capacity)
    {
        _elements = new T[capacity];
    }

    public bool IsEmpty 
    { 
        get {return _size == 0;}
    }

    public int Size { 
        get {return _size;}
    }

    public void Push(T element)
    {
        if(_size == _elements.Length) 
            throw new OverFlow();

        _elements[_size++] = element;
    }

    public T Pop()
    {
        if(_size == 0)
            throw new UnderFlow();

        return _elements[_size--];
    }

    public class UnderFlow: Exception
    {

    }

    public class OverFlow: Exception
    {

    }
}

اینجا یه دونه Stack رو implement کردیم. قبل از اینکه ادامه ی داستان رو بخونی, خودت بگو که هر کدوم از متد ها چه جورین؟ و Side Effect دارن یا نه ؟

isEmpty و size

خب این دو متد یا Property هیچ تاثیری روی Instance Filed های کلاس نمیذارن و state کلاس رو تغییر نمیدن. کاری که میکنن این هست که صرفن دارن مقدار _size رو میخونن و یا روش query میزنن. پس Side Effect ندارن.

push و pop

در طرف مقابل این دو متد دارن state کلاس رو تغییر میدن. چی؟

این دو متد دارن روی _elements تغییر ایجاد میکننو یه چیزی رو بهش اضافه میکنن و یا ازش کم میکنن. پس state کلاس رو دست مالی میکنن. پس این دو تا Side Effect دارن!!!.

سوال مهم؟

آیا Side Effect فقط محدود به این چیزایی میشه که تا الان دیدیم؟

یعنی اگه یه متدی Instance Field های کلاس رو تغییر داد, میگیم Side Effect داره و اگه نه, میگیم نداره؟

نه.

گفتیم که هر موقع متد یا فانکشن مورد نظر یه چیزی خارج از خودش رو تغییر بده میگیم Side Effect داره. یکی از مواردش میتونه state کلاس باشه, ولی فقط به این مورد محدود نمیشه.

برای مثال متد یا فانکشن مورد نظر ممکنه یه تغییری توی دیتابیس ایجاد کنه و یه record به دیتابیس اضافه کنه.

یا ممکنه یه چیزی رو داخل یه File بنویسه.

یا ممکنه یه متدی داری که داره یه request ای رو میفرسته سمت سرور و در نهایت توی دیتابیسی که سمت سرور هست یه تغییر ایجاد میشه.

تمام این موارد, مثال هایی از Side Effect هستن. چون متد مورد نظر داره یه Shared State رو تغییر میده.

یکم بیشتر در مورد دیتابیس صحبت کنیم. هر موقع میخوایم با دیتابیس کار کنیم, در ابتدا باید یه Connection به دیتابیس بزنیم و بعد کار مورد نظرمون رو انجام بدیم و در نهایت connection رو close کنیم.

خب اگه هدفمون Write کردن روی دیتایبس باشه(یعنی operation های Create یا Update یا Delete), خب مشخصن متدمون Side Effect داره, چرا که داریم یه چیزی رو توی دیتابیس تغییر میدیم.

حالا اگه هدفمون فقط Read کردن یا Query زدن باشه, چی؟ اینجا Side Effect نداریم. چون که چیزی رو تغییر ندادیم. ولی شرط داره. Open کردن Connection و Close کردنش, خودشون Side Effect هستن و دارن یه چیزی رو تغییر میدن!!! ولی چون ابتدای متد Open کردیم و انتهای اون Close کردیم و پس از کامل شدن اجرای متد, همه چیز مثل اولش هست, میتونیم بگیم که Side Effect نداریم.

پ.ن: البته چنین فانکشنی(یعنی query زدن روی database), با وجود Side Effect نداشتن, یه Pure Function یا Mathematical Function نیست. ولی خب این موضوع ارتباطی با بحث امروزمون نداره.

CQS

حالا با این همه مقدمه چینی, ببینیم CQS چی میگه؟

این اصل میگه که در ایده آل ترین حالت (توجه کن در ایده آل ترین حالت), باید signature متد یا فانکشنت جوری باشه که با نگاه کردن بهش بفهمی که اون متد یا فانکشن Side Effect داره یا نه.

به همین منظور متد ها رو به دو دسته تقسیم میکنیم:

  • Command
  • Query

Command

این های متد هایی هستن که Side Effect دارن.

Side Effect اشون میتونه هر چیزی باشه. از تغییر state کلاس گرفته تا تغییر توی دیتابیس و … .

return type این متد ها باید void باشه و تنها هدف از کال کردنشون همون Side Effect باشه. نباید هیچی ریترن کنن.

پ.ن: setter های یه کلاس توی این دسته قرار میگیرن.

Query

این ها متد هایی هستن که Side Effect ندارن و صرفن یه چیزی رو calculate و ریترن میکنن. مثلن یه query روی دیتابیس میزنن. یا یه محاسباتی رو انجام میدن یا … .

return type این متد ها void نیست و یه چیزی رو ریترن میکنن (نتیجه محاسبات یا query).

پ.ن: getter های یه کلاس توی این دسته قرار میگیرن.

نتیجه

CQS میگه که متد هات رو از نظر اینکه Side Effect دارن یا نه به دو دسته تقسیم کن (Command و Query) و تا جایی که میتونیاز هم جداشون کن و اگه یه متدی قراره یه چیزی رو calculate کنه و در نهایت نتیجه رو ریترن کنه, بهتره که Side Effect نداشته باشه و در مقابل متدی که قراره Side Effect داشته باشه, بهتره که به جز اون Side Effect کار دیگه ای رو انجام نده و return type اش هم void باشه.

اینجوری متدهای کلین تری خواهیم داشت و تست نویسی هم راحتر میشه.

آیا همیشه باید این اصل رو رعایت کنیم؟

نه.

مثل خیلی از اصول دیگه, این اصل هم یه اصل ایده آل هست و امکان اینکه همیشه و همه جا رعایتش کنیم عملی نیست. ولی باید اون رو همیشه گوشه ی ذهن خودمون داشته باشیم و تا جایی که میتونیم ازش پیروی کنیم.

یه نمونه نقض CQS رو توی مثال های همین مقاله داشتیم. اگه گفتی کدوم مثال؟

افرین…Stack.

  • Dart
  • C#

base class Stack<T>{

  final List<T?> _elements;
  int _size = 0;

  Stack(int capacity) :
    _elements = List.filled(capacity, null);

  bool get isEmpty => _size == 0;
  int get size => _size;

  void push(T element){
    if(_size == _elements.length){
      throw OverFlow();
    }

    _elements[_size++] = element;
  }

  T pop(){
    if(_size == 0){
      throw UnderFlow();
    }

    return _elements[_size--]!;
  }
  
}

base class UnderFlow implements Exception{}
base class OverFlow implements Exception{}
public class Stack<T>
{
    private T[] _elements;
    private int _size = 0;

    public Stack(int capacity)
    {
        _elements = new T[capacity];
    }

    public bool IsEmpty 
    { 
        get {return _size == 0;}
    }

    public int Size { 
        get {return _size;}
    }

    public void Push(T element)
    {
        if(_size == _elements.Length) 
            throw new OverFlow();

        _elements[_size++] = element;
    }

    public T Pop()
    {
        if(_size == 0)
            throw new UnderFlow();

        return _elements[_size--];
    }

    public class UnderFlow: Exception
    {

    }

    public class OverFlow: Exception
    {

    }
}

متد pop دارای Side Effect هست. پس یه command محسوب میشه. پس باید return type اش void باشه!!!

ولی شوربختانه اینجوری نیست و داره یه چیزی رو ریترن میکنه.

خب چه کار کنیم؟ تغییرش بدیم؟

نه.

این نوع پیاده سازی سال ها و دهه هاست که همه جا رایجه و همه باهاش خو گرفتن و تغییر دادنش بیشتر از اینکه نفع داشته باشه, ضرر داره.

پس بازم نکته ی همیشگی خودم رو تکرار میکنم. ما اصول مختلف رو یاد میگیریم تا بهمون در کدنویسی بهتر کمک کنن. در نهایت ما انسان هستیم و هوش داریم. هدف از رعایت اصول, کد نویسی بهتر هست. نباید خنگ بازی در بیاریم و همیشه و همه جا فقط و فقط بخوایم اصول رو رعایت کنیم. اگه به بهتر شدن کدمون کمک کردن…عالیه. در غیر این صورت …. .

دیدگاهتان را بنویسید

error: Alert: Content is protected !!