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 generują 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.balance.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 money: Money;

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

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

  balance = (): Money => this.money;
}
W takim przypadku deposit zwraca void-a, a nowa metoda balance aktualny stan portfela. Dzięki temu zabiegowi rozdzieliliś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:

KISS: keep it simple, stupid!

Zasada KISS: "Keep it simple, stupid", może zostać dosłownie przetłumaczona na: "rób to prosto, głupku". Mówi ona o tym, abyśmy tworzyli kod w jak najprostszy i najbardziej czytelny sposób. Już dziś sprawdź, czego się wystrzegać, aby spełniać KISS.

Sprawdź ten wpis

Wzorce projektowe

Wzorce projektowe zostały stworzone po to, aby nie wymyślać przysłowiowego koła na nowo. Znajomość wzorców projektowych i umiejętność ich stosowania pozwala na szybkie rozwiązywanie problemów. Wpis ten radzi, jakie wzorce zastosować u siebie.

Sprawdź ten wpis

Wzorzec singleton

Kolejny kreacyjny wzorzec projektowy omawiany na łamach tego bloga: singleton. Wzorzec dookoła którego narosło wiele mitów i legend. Dziś o tym dlaczego singleton jest antywzorcem, jakie problemy powoduje oraz kiedy warto po niego sięgnąć.

Sprawdź ten wpis

Wzorzec stan (state)

Dziś prezentuję kolejny wzorzec projektowy, który pozwala uniknąć drabinek if-else if. Mowa tutaj o wzorcu state, który idealnie nada się, jeśli posiadasz różne stany w swoim systemie oraz chcesz mieć możliwość płynnego przechodzenia pomiędzy nimi.

Sprawdź ten wpis

Wzorzec pełnomocnik (virtual proxy)

Wzorzec virtual proxy służy do tworzenia obiektów pośrednich, które nadzorują dostęp do obiektów dla których są pełnomocnikami. Dzięki temu, że pełnomocnik i klasa bazowa udostępniają jednakowy interfejs, możemy ukryć w pośredniku dodatkową logikę.

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.