CQS – (ang. Comand Query Separation) jest to jedną z podstawowych zasad stosowanych w programowaniu obiektowym, której fundamentalną zaletą jest bardzo pozytywny wpływ na czytelność kodu.
Brzmi ona następująco:
Każda metoda powinna zwracać informacje o stanie obiektu bez jego modyfikacji, bądź modyfikować stan obiektu bez zwracania informacji o nim.
Operując bardziej przyziemnym językiem możemy powiedzieć, że zadane pytanie nie powinno wpływać na towarzyszącej mu odpowiedzi.
CQS – przykład książkowy
Na listingu poniżej zobaczyć możemy metodę send(...)
zwracającą wynik typu bool
. W samym ciele tej metody możemy znaleźć jednak kilka miejsc, w których modyfikujemy stan obiektu, przez zmianę wartości właściwości lastErrorMessage
:
<?php
class MessageSender
{
/* Fields and constructor */
public function getLastErrorMessage(): bool
{
return $this->lastErrorMessage;
}
public function send(Message $message): bool
{
if ($message->isValid() === false) {
$this->lastErrorMessage = 'Invalid message found';
return false;
}
if ($message->isEmpty() === true) {
$this->lastErrorMessage = 'Empty message found';
return false;
}
switch ($message->getType()) {
case Message::TYPE_EMAIL: {
return $this->emailSender->send($message);
} break;
case Message::TYPE_PUSH: {
return $this->pushNotificationSender->send($message);
} break;
}
$this->lastErrorMessage = 'Unknown error found';
return false;
}
}
Zgodnie z prawem CQS, powinniśmy oddzielić od siebie zwracanie wartości oraz zmianę stanu obiektu. Zatem w celu sprawdzenia, czy wiadomość została wysłana, powinniśmy utworzyć osobną metodę:
<?php
class MessageSender
{
/* Fields and constructor */
public function getLastSentStatus(): bool
{
return $this->lastMessageSentStatus;
}
public function getLastErrorMessage(): string
{
return $this->lastErrorMessage;
}
public function send(Message $message): void
{
$this->lastMessageSentStatus = false;
if ($message->isValid() === false) {
$this->lastErrorMessage = 'Invalid message found';
return;
}
if ($message->isEmpty() === true) {
$this->lastErrorMessage = 'Empty message found';
return;
}
switch ($message->getType()) {
case Message::TYPE_EMAIL: {
$this->lastMessageSentStatus = $this->emailSender->send($message);
return;
} break;
case Message::TYPE_PUSH: {
$this->lastMessageSentStatus = $this->pushNotificationSender->send($message);
return;
} break;
}
$this->errorMessage = 'Unknown error found';
}
}
Podsumowując, standardową operacją związaną z CQS będzie wydzielenie osobnego pola oraz gettera dla zwracanej wartości.
CQS a LazyLoad
Bardzo ciekawym przypadkiem w kontekście CQS są metody korzystające ze wzorca LazyLoad . Przyjrzyjmy się poniższemu fragmentowi kodu:
<?php
class ApiRequestSender
{
/* Fields and constructor */
public function sendRequest(Request $request): Response
{
if ($this->client === null) {
$this->client = $this->clientFactory->createNew();
}
return $this->client->request($request->getMethod(), $request->getBody());
}
}
Podobnie jak w poprzednim przykładzie, zmieniamy stan obiektu, przy równoczesnym zwracaniu wartości przez metodę. Jeżeli w dalszym ciągu chcemy być zgodni z zasadą CQS, to powinniśmy ponownie wydzielić metodę zmieniającą stan obiektu:
<?php
class ApiRequestSender
{
/* Fields and constructor */
public function initialize(): void
{
if ($this->client === null) {
$this->client = $this->clientFactory->createNew();
}
}
public function sendRequest(Request $request): Response
{
if ($this->client === null) {
throw new ClientNotInitializedException();
}
return $this->client->request($request->getMethod(), $request->getBody());
}
}
Jak możemy jednak zauważyć, straciliśmy zaletę wzorca LazyLoad
. Rozwiązaniem, które okaże się pomocne, jest utworzenie klasy dekorującej, która zainicjalizuje klienta API przed jego użyciem:
<?php
class ApiRequestWrapper
{
/* Fields and constructor */
public function sendRequest(Request $request): Response
{
$this->requestSender->initialize();
return $this->requestSender->request(Request);
}
}
Powyższe rozwiązanie jest zgodne z zasadą CQS, ponieważ obiekt klasy ApiRequestWrapper
nie zostaje zmieniony wewnątrz metody sendRequest(...)
(zmieniony zostaje obiekt requestSender
, co ze względu na przechowywanie obiektów w postaci referencji nie jest tożsamym ze zmianą obiektu nadrzędnego).
Zalety stosowania CQS
Podejmowany w tym wpisie wzorzec niemalże od razu przynosi nam spory zestaw zalet:
- Korzystając z metod, możemy z góry przewidzieć skutki ich użycia. Jeżeli cały nasz kod będzie zgodny z CQS, to pozbędziemy się nieprzyjemnych skutków ubocznych, niejednokrotnie powiązanych z wydajnością tworzonej aplikacji
- Dbamy o jakość kodu poprzez tworzenie mniejszych objętościowo metod, przez co nasz kod będzie łatwiejszy w analizie
- Tworzony przez nas kod będzie bardziej semantyczny – korzystanie z zasady CQS w dłuższej perspektywie prowadzi do znacznie mniej anemicznego modelu domenowego.
- CQS bardzo mocno wpływa na poprawę czytelności kodu
- Aplikacja będzie łatwiej testowalna, a testy staną się łatwiejsze w analizie
- Dodatkowo, wprowadzając ten wzorzec do naszej aplikacji nie siejemy rewolucji, dzięki czemu refactoring staje się bardziej bezpieczny
Podsumowanie
Wzorzec CQS jest jednym z najprostszych wzorców projektowych, który możemy wprowadzić do naszego kodu. Dzięki jego prostocie oraz niskiemu kosztowi refaktoryzacji możemy bardzo szybko wprowadzić go do tworzonych przez nas aplikacji, przez co niemalże od razu możemy cieszyć się lepszym w analizie oraz bezpieczniejszym w użyciu kodem.
Comments are closed.