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.

Profesjonalista, zajmujący się na co dzień aplikacjami biznesowymi w ekosystemie PHP. Jego pasją jest odkrywanie nowych konceptów programistycznych oraz wzorce architektoniczne. Uwielbia również pisać testy, gdyż jak sam uważa, dobry kod to przetestowany kod.

Comments are closed.