Command-Query Separation (CQS)

Command-Query Separation, czyli zasada o rozdzielaniu zadań metod tak, aby były jedynie komendami lub zapytaniami. Jeżeli Twoja metoda realizuje logikę i zwraca wyniki, lub podczas pobierania danych wykonuje side taski, to łamie ona CQS.


Wszędzie podziały

Tematyka dzisiejszego wpisu zalicza się do tych z rodzaju must-have każdego programisty. W poprzednich wpisach niejednokrotnie wspominałem już o zasadzie CQS, czyli Command-Query Separation. Przyszedł w końcu czas na wyjaśnienie, o co tak naprawdę w rozdzielaniu na komendy i zapytania chodzi. Otóż metody klas powinny być dzielone na te, które wykonują jakąś logikę (komendy) oraz na te, które służą jedynie do odczytu danych (zapytania). Mieszanie odpowiedzialności w taki sposób, że komendy zwracają jakieś dane, czy też zapytania generuja efekty poboczne, skutkuje łamaniem zasady CQS

Autorem wspomnianego podejścia jest 
Bertrand Meyer, który pierwotnie wprowadził je dla stworzonego przez siebie języka programowania: Eiffel. Oczywiście wszystko co dobre w świecie IT prędzej czy później rozprzestrzeni się na inne technologie, tak też stało się tym razem.  Dodatkowo rozprzestrzenienie to spowodowało powstanie nowych, podobnych i często bazujących na CQS zasad. Wyróżnić możemy na przykład CQRS, który jest swoistego rodzaju rozwinięciem CQS-a, ale o tym porozmawiamy innym razem.

Kod spełniający CQS

Przejdźmy teraz do krótkiego przykładu ukazującego, jak wygląda łamanie CQS-a. Poniżej widzimy klasę Wallet, która posiada parametr balance reprezentujący aktualny stan portfela. Wallet udostępnia metodę deposit, która służy do włożenia pieniędzy do portmonetki. Realizuje ona logikę dodania pieniędzy, a następnie zwraca aktualny stan.

export class Wallet {
  private balance: Money;

  deposit(money: Money): Money {
    if (this.money.currency !== money.currency) {
      throw new Error('Currency missmatch.');
    }

    this.balance = this.balance.add(money);

    return this.balance;
  }
}
Jak pewnie się domyślasz reguła CQS została złamana tym nieszczęsnym returnem zwracającym balance. Aktualnie metoda deposit jest zarówno komendą, jak i zapytaniem, ponieważ realizuje logikę i zwraca dane. Poprawić ją możemy poprzez podział jej obowiązków na command oraz query, przeniesione do nowej metody.

export class Wallet {
  private balance: Money;

  deposit(money: Money): void {
    if (this.money.currency !== money.currency) {
      throw new Error('Currency missmatch.');
    }

    this.balance = this.balance.add(money);
  }

  balance = (): Money => this.money;
}
W takim przypadku deposit zwraca void-a, a nowa metoda balance aktualny stan portfela. Dzięki temu zabiegowi rozdzielilśmy dwie odpowiedzialności metody dodającej pieniądze do portfela.

Command nie może nic zwrócić?

CQS mówi o tym, aby komendy nie służyły jako zapytania czyli, aby nie zwracały np. danych na których operowała pewna logika. Reguła Command-Query Separation nie zabrania jednak, aby command zwrócił rezultat swojego działania, czy też odnośnik do nowo powstałego lub zaktualizowanego bytu, np. w postaci uuid.

Osobiście, jeżeli zachodzi potrzeba zwrócenia informacji o wyniku działania komendy, to używam w tym celu dzielonej pomiędzy modułami "pseudo krotki". Przyjmuje ona postać klasy o nazwie Result z trzema polami: success, message oraz data. Prawdopodobnie nazwa pola data jest trochę myląca i może bardziej zasadniejsze byłoby nazwanie go, jako identifier, link? Niemniej jednak takie rozwiązanie może wyglądać następująco:

export class Result {
  private constructor(
    public readonly success: boolean,
    public readonly message?: string,
    public readonly data?: any
  ) {}

  static success = (message?: string, data?: any): Result =>
    new Result(true, message, data);

  static error = (message?: string, data?: any): Result =>
    new Result(false, message, data);
}

export class SomeService {
  doSth(): Result {
     // logic

     if (error) {
       return Result.error(error.message);
     }

     return Result.success(result.message, id);
  }
}
Innym odstępstwem od reguły CQS mogą być wzorce kreacyjne, np. factory czy builder, które w zamyśle zwracają obiekt na którym prowadziły wcześniej operacje. Złamanie reguły CQS nie jest pogwałceniem praw człowieka, jednak podczas łamania zasad trzeba być świadomym korzyści i konsekwencji z tego płynących.

Podsumowanie




Polecane wpisy:

Wzorzec budowniczy (builder)

Wzorzec projektowy budowniczy służy do dynamicznej budowy obiektów. Pozwala na kreowanie różnych typów obiektu w zależności od przesłanych wymagań. Dzięki temu posiadamy centralny punkt odpowiedzialny jedynie za tworzenie wariacji danego typu.

Sprawdź ten wpis

Wzorzec metoda wytwórza (factory metod)

Jeżeli masz do czynienia z tworzeniem różnych obiektów bazujących na predefiniowanym modelu, to powinieneś/aś skorzystać z metody wytwórczej. Kreacyjny wzorzec projektowy factory method pozwala być fancy w tworzeniu obiektów.

Sprawdź ten wpis

SOLID z przykładami w TypeScript

SOLID (Single responsibility principle, Open/closed principle, Liskov substitution principle, Interface segregation principle, Dependency inversion principle), czyli pięć zasad programowania obiektowego, które każdy powinien przestrzegać.

Sprawdź ten wpis

Jak zostać architektem oprogramowania?

Aby projektować dobrą architekturę oprogramowania trzeba najpierw zaznajomić się z jej przesłankami, rodzajami, a także sposobami wdrażania. Architektura nie jest przecież czymś stałym, co wszędzie implementowane jest w jednakowy sposób.

Sprawdź ten wpis

Kompozycja zamiast dziedziczenia (composition over inheritance)

Czym jest podejście composition over inheritance? Dzisiaj o tym, dlaczego każdy powinien przemyśleć czy dziedziczenie klas w jego projekcie jest naprawdę potrzebne. Prawdopodobnie powinieneś ograniczyć dziedziczenie klas na rzecz ich kompozycji.

Sprawdź ten wpis

Autor wpisu:

Gabriel Ślawski

Fanatyk czystego i prostego kodu. Zwolennik podejść DDD oraz Modular Monolith. Na codzień pracuje jako programista i architekt. Po godzinach spełnia się w projektach open source, udziela się na blogach oraz czyta książki o kosmosie i astrofizyce.