Chain of responsibility

Cześć, w dzisiejszym wpisie przedstawię Ci na prostym przykładzie czym jest i po co się stosuje wzorzec projektowy chain of responsibility. Wzorzec ten mam nadzieje że pomoże Ci rozwiązać wiele przypadków oraz pozostawić kod dużo czystszy i otwarty na rozszerzenia 🙂

Całość kodu możesz znaleźć tutaj. (Niektóre paczki/konfiguracje etc. są zbędne, dla własnej wygody przykłady buduję na podstawie swojego skeletona ;))

Słowem wstępu o wzorcu

Chain of responsibility ma nam pomóc gdy mamy łańcuch np. komend dla danej funkcjonalności. Każde ogniwo decyduje czy ma zostać wykonanego następne ogniwo czy zakończyć działanie.

 

Przykładem takiego łańcucha może być import danych np. produktów. Poniżej przedstawię Ci prosty przykład jaką odpowiedzialność może mieć każde ogniwo.

  • 1: Sprawdź dane
  • 2: Importuj atrybuty produktu
  • 3: Importuj cenę produktu
  • n: […]

Przejdźmy do praktyki

Aby przykład był przejrzysty przedstawię Ci kryteria akceptacyjne funkcjonalności

  • użytkownik ma możliwość stworzenia postu
  • po stworzeniu postu następuje wysyłka newslattera do wszystkich subskrybentów

Feature będzie bardzo banalny ale można prosto na nim się przejechać.

Co mogę zrobić tu źle

Gdy aplikacja jest bardzo prosta to znasz jej każdy kawałek kodu ale w momencie gdy projekt zaczyna się rozrastać firma ma kilkunastu deweloperów rozpoczyna się droga pod górkę. Wchodzisz do pierwszego handlera a tam łata na łacie, SOLID, KISS, DRY etc. to postacie z mitologii no i finalnie nie wiesz co tu się dzieje więc co robisz? Dodajesz kolejnego if’a wywołanie metody i zadowolony oddajesz zadanie. W tym momencie przyczyniasz się do tworzenia własnego piekła!

Wróćmy teraz do naszych kryteriów akceptacji i zastanówmy się co tu można skopać. Nic Ci nie przychodzi do głowy? Nic się nie dzieje. 🙂

<?php

namespace App\CommandHandler;

use App\Command\CreatePostCommand;
use App\Command\SendNewsletterCommand;
use App\Entity\Post;
use App\Repository\NewsletterUserRepository;
use App\Repository\PostRepository;
use App\Util\ChainHandlerAbstract;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Notifier\Notification\Notification;
use Symfony\Component\Notifier\NotifierInterface;
use Symfony\Component\Notifier\Recipient\Recipient;

#[AsMessageHandler]
final class CreatePostCommandHandler
{
    public function __construct(
        private readonly PostRepository $postRepository,
        private readonly NewsletterUserRepository $newsletterUserRepository,
        private readonly NotifierInterface $notifier,
    ) {
    }


    public function __invoke(CreatePostCommand $command)
    {
        $post = new Post($command->getUuid(), $command->getTitle());

        $this->postRepository->add($post);

        $this->sendNewsletter($post);
    }

    private function sendNewsletter(Post $post): void
    {
        foreach ($this->newsletterUserRepository->findAll() as $user) {
            $notification = (
            new Notification(
                'New Post on blog lukaszstaniszewski.pl!',
                ['email'],
            )
            )->content($post->getTitle());

            $recipient = new Recipient($user->getEmail());

            $this->notifier->send($notification, $recipient);
        }
    }
}

W powyższym przykładzie na start złamaliśmy SOLID a dokładniej SRP (Single Responsibility Principle). Nasza klasa spełnia kryteria akceptacyjne oraz w teorii wszystkie zależności są obszarze tworzenia postu ale tak naprawdę nie jest. Nasza klasa ma dwie odpowiedzialności (tj. również dwa powody do zmiany) pierwszą jest stworzenie użytkownika a drugą wysyłka newsletteru a za niedługo będzie miała pewnie kolejne i kolejne.

Jak to zrobić lepiej z chain of responsibility

Na ratunek przychodzi nam tutaj wymieniony w tytule wzorzec projektowy

<?php

namespace App\CommandHandler;

use App\Command\CreatePostCommand;
use App\Command\SendNewsletterCommand;
use App\Entity\Post;
use App\Repository\PostRepository;
use App\Util\ChainHandlerAbstract;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\MessageBusInterface;

#[AsMessageHandler]
final class CreatePostCommandHandler extends ChainHandlerAbstract
{
    public function __construct(
        MessageBusInterface $commandBus,
        private readonly PostRepository $postRepository
    ) {
        parent::__construct($commandBus);
    }


    public function __invoke(CreatePostCommand $command)
    {
        $post = new Post($command->getUuid(), $command->getTitle());

        $this->postRepository->add($post);

        $this->next(new SendNewsletterCommand($command->getUuid()));
    }
}
<?php

declare(strict_types=1);

namespace App\CommandHandler;

use App\Command\SendNewsletterCommand;
use App\Repository\NewsletterUserRepository;
use App\Repository\PostRepository;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Notifier\Notification\Notification;
use Symfony\Component\Notifier\NotifierInterface;
use Symfony\Component\Notifier\Recipient\Recipient;

#[AsMessageHandler]
final class SendNewsletterCommandHandler
{
    public function __construct(
        private readonly NewsletterUserRepository $newsletterUserRepository,
        private readonly PostRepository $postRepository,
        private readonly NotifierInterface $notifier,
    ) {
    }

    public function __invoke(SendNewsletterCommand $command): void
    {
        $post = $this->postRepository->getByUuid($command->getPostUuid());

        foreach ($this->newsletterUserRepository->findAll() as $user) {
            $notification = (
                new Notification(
                    'New Post on blog lukaszstaniszewski.pl!',
                    ['email'],
                )
            )->content($post->getTitle());

            $recipient = new Recipient($user->getEmail());

            $this->notifier->send($notification, $recipient);
        }
    }
}

Jak widzisz powyżej rozdzieliśmy odpowiedzialność na dwa osobne handlery. Pierwszy tworzy sam post drugi zaś obsługuje nam wysyłkę newslettera ale to nie koniec korzyści.

 

Gdy przyjdzie do nas Pan lub Pani z działu marketingu i powie „Hej, potrzebowałbym uruchomienie kampanii na fb w momencie dodania postu” dla nas to nic trudnego wystarczy że dodamy kolejny handler oraz go wepniemy w odpowiednie miejsce (wiem, wiem kampania fb to nierealne wymaganie dla takiego przykładu oraz sam przykład ma wiele wad ale ma on tylko przedstawiać idee chain of responsibility :)).

Podsumowanie

Jak widać całe nasze programowanie głównie kręci się wokół prostych zasad tj. KISS, DRY, GRASP, YAGNI, SOLID a wzorce projektowe takie jak chain of responsibility mają nas po prostu wesprzeć.

W momencie gdy zawsze myślimy o tych zasadach nasz świat programowania staję się dużo prostszy.

Mam nadzieję że dobrze przedstawiłem Ci podstawową idee łańcucha zobowiązań a jeśli masz jakieś uwagi, chcesz o coś dopytać śmiało pisz na maila lub napisz do mnie na linkedinie. 😉

Zachęcam Cię również do przeczytania wpisu o prototype.

Źródła:

Chain of responsibility
Przewiń na górę