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
- CQS to niewątpliwie zasada, która powinna być znana i stosowana przez każdego.
- Przy stosowaniu Command-Query Separation nasz kod staje się czytelny i łatwy w zrozumieniu.
- Komendy mogą zwracać wynik swojego działania lub referencję do bytu na którym operowały.
Dodaj komentarz