Wzorców projektowych jest bardzo dużo, nawet jeżeli jakoś je pogrupujemy. A pogrupowałem już we wcześniejszym wpisie, którego temat będę kontynuował tutaj. Zapraszam więc na drugą część wpisu o wzorcach projektowych przyjaznych zasadzie OCP 🙂
Obserwator
Przyznam, że dawno nie widziałem obserwatora w akcji logiki biznesowej. Jest to wzorzec bardziej wykorzystywany w technologiach, gdzie można coś poklikać (GUI, gry komputerowe itp). Pomimo jego rzadkiego stosowania, idealnie wpisuje się on w dzisiejszy temat.
Wzorzec Obserwatora polega na dołączaniu, do klasy realizującej jakieś ważne zadanie (klasa obserwowana), obiektów obserwujących jej procesy biznesowe. Każdy obserwator powinien implementować ten sam interfejs, aby móc uprościć to, o co tutaj chodzi najbardziej – wywołanie odpowiedniej metody obiektu obserwującego w odpowiednim miejscu, z odpowiednimi parametrami. Prześledźmy przykładową implementację tego wzorca:
<?php
namespace App;
final class UserProfileUpdater
{
/** @var ProfileUpdateObserverInterface[] */
private array $observers = [];
// Constructor ...
public function attach(ProfileUpdateObserverInterface $observer): void
{
$this->observers[] = $observer;
}
public function updateUserProfile(ProfileDTO $profileDto): void
{
$profile = $this->userProfileRepository->findByEmail($profileDto->getEmail());
// Logic for updating user profile ...
foreach ($this->observers as $observer) {
$observer->userProfileEdited($profileDto, $profile);
}
}
}
interface ProfileUpdateObserverInterface
{
public function userProfileEdited(
ProfileDTO $inputData,
ProfileInterface $profile
): void;
}
final class SendProfileUpdatedEmail implements ProfileUpdateObserverInterface
{
// Constructor ...
public function userProfileEdited(
ProfileDTO $inputData,
ProfileInterface $profile
): void {
$this->mailer->send('ProfileUpdatedEmail.html.twig', $profile->getEmail());
}
}
final class SendPasswordHasBeenChangedEmail implements ProfileUpdateObserverInterface
{
// Constructor ...
public function userProfileEdited(
ProfileDTO $inputData,
ProfileInterface $profile
): void {
if ($profileDto->getPassword() === null) {
return;
}
$this->mailer->send('PasswordHasBeenUpdated.html.twig', $profile->getEmail());
}
}
Powyższy przykład pokazuje dwie klasy, które są zainteresowane zmianą profilu użytkownika. Jako, że bloguję o Symfony, to do połączenia serwisów ze sobą (wywołania metod attach(...)
wykorzystałbym Symfony Compiler Pass. Każdy serwis implementujący ProfileUpdateObserverInterface
otrzymałby ode mnie ten sam tag, po którym klasa Compiler Passa je sobie pobierze, a następnie pouruchamia, co trzeba.
Tytułowa przyjazność Open-Close Principle wyniknie tutaj z tego, że jeżeli będziemy chcieli dodać nową logikę, która będzie zainteresowana edycją profilu (np. stworzenie miniaturek uploadowanego zdjęcia profilowego), to wystarczy dorzucić klasę implementującą interfejs ProfileUpdateObserverInterface
, wyczyścić cache i voila! 🙂
Symfony Event Dispatcher
Czy wiedzieliście, że Obserwatora możemy znaleźć również w Symfony? Jeżeli nie, to wejdźcie na stronę Event Dispatchera i wczytajcie się w opis:
The Symfony EventDispatcher component implements the Mediator and Observer design patterns to make all these things possible and to make your projects truly extensible.
Symfony docs – https://symfony.com/doc/current/components/event_dispatcher.html
Oprócz Obserwatora, mamy tu również wzorzec Mediatora, który nieco nam zaciemni obraz, no ale trudno.
Zatem, cała bajka zawsze zaczyna się, kiedy do serwisu wrzucamy w zależność klasę implementującą Symfonowy interfejs EventDispatcherInterface
. Gdzieś w naszej logice wypuszczamy linijkę:
$this->eventDispatcher->dispatch($event, 'UserProfileHasBeenUpdated');
Dalej uruchamia się logika Event Dispatchera, która chodzi po listenerach, uruchamiając te, które są zainteresowane naszym zdarzeniem:
// https://github.com/symfony/event-dispatcher/blob/6.3/EventDispatcher.php
public function dispatch(object $event, string $eventName = null): object
{
$eventName ??= $event::class;
if (isset($this->optimized)) {
$listeners = $this->optimized[$eventName] ?? (empty($this->listeners[$eventName]) ? [] : $this->optimizeListeners($eventName));
} else {
$listeners = $this->getListeners($eventName);
}
if ($listeners) {
$this->callListeners($listeners, $eventName, $event);
}
return $event;
}
Ostatnim punktem układanki jest odpowiedź pytanie: „Skąd EventDispatcher wie, które listenery uruchomić?”. Otóż, jak nam wszystkim wiadomo, praca z Event Dispatcherem dzieli się na etap związany z logiką biznesową oraz kontenerem zależności, w którym definiujemy to, który listener nasłuchuje na które zdarzenie. Skoro mamy kontener DI, to w takim razie musi być też odpowiedni Compiler Pass, który to wszystko połączy ze sobą.
Message Bus
Kolejnym wzorcem (choć niekoniecznie) na naszej liście będzie Message Bus. Kontrowersyjnym jest to, czy można go w sumie zaliczyć do grona wzorców projektowych. Bardziej doświadczeni wiedzą, że jest to raczej architektura komunikacyjna stosowana w aplikacjach mikroserwisowych.
Komunikacja między serwisami może odbywać się poprzez wysyłkę wiadomości na tzw. szynę (ang. bus), która następnie może zostać przeczytana oraz odpowiednio zinterpretowana przez pozostałe systemy połączone do tej szyny. Na szczęście Symfony Messenger pokazał nam, że można tego rodzaju zachowanie (w nieco zubożonej wersji) przenieść na poziom pojedynczej aplikacji. Tego typu podejście stosuje się najczęściej w Modularnym Monolicie, którego komponenty możemy następnie wydzielać do postaci osobnych mikroserwisów.
Popularność wykorzystywania Message Busa w aplikacjach biznesowych jest bardzo duża. Pozwala ona na wewnętrzną implementację wzorców architektonicznych, które właśnie przez powiązanie z Message Busem powodują, że całość jest bardzo przyjazna Open-Close Principle.
CQRS
CQRS jest architekturą, która dzieli aplikację (bądź jej fragment) na dwie części. Pierwsza część jest związana z wydawaniem komend, które następnie zostają przetworzone w celu zmiany stanu systemu. To w tym miejscu znajduje się cała logika biznesowa, czyli zbiór wszystkich reguł, dzięki którym możemy realizować nasz biznes. Druga część aplikacji to zapytania, które w żaden sposób nie są połączone z logiką biznesową. Ich rolą jest jak najszybsze zwrócenie informacji dotyczących wysłanym im zapytaniom. Samemu wzorcowi CQRS poświęciłem osobny wpis, do którego przeczytania Was serdecznie zachęcam.
To, co łączy CQRS z dzisiejszą tematyką, to niewątpliwie rozszerzalność systemu. Jeżeli chcemy dodać nowe zachowania systemu, bądź chcemy zapytać system o rzeczy, o które dotąd nie pytaliśmy – możemy to zrealizować poprzez dodanie prostej klasy komendy / zapytania, która będzie obsługiwana przez (również) nowo dodaną klasę ją obsługującą. W przypadku zapytań będziemy potrzebowali dorzucić również nowe klasy modelu, do którego przekażemy zebrane informacje. To, czego Open-Close nie lubi, czyli modyfikowanie systemu, będzie tutaj realizowane tylko wtedy, kiedy będziemy chcieli zmienić istniejące już zachowanie.
Architektura sterowana zdarzeniami
Możliwość wykorzystania uproszczonej wersji Message Busa otwiera nam drogę do rozszerzania systemu nie tylko w formie „zrób to” (CQRS), ale również „zostało zrobione”. Z jednej strony, może wydawać się, że jest to jedynie semantyka. Bo zarówno w jednym jak i drugim scenariuszu wpuszczamy na szynę wiadomość, która następnie zostanie obsłużona przez jakieś listenery. Z drugiej strony, są drobne różnice, które robią robotę.
Kiedy mówimy o wysłaniu komendy na szynę, to powinniśmy mieć z tyłu głowy, że może ona zostać obsłużona tylko i wyłącznie przez jeden mechanizm (handler/listener). Jeżeli nie istnieje w systemie żaden byt, który by tą komendę miał obsłużyć, to żądanie powinno zakończyć się jakąś formą błędu. Ze zdarzeniami jest trochę inaczej. My wypuszczamy na szynę zdarzenie, ale nie musi istnieć żaden element nasłuchujący na nie. Zdarzenie jest jedynie faktem, który wydarzył się w przeszłości. Jest to informacja, która niekoniecznie musi kogoś interesować. Dodatkowo, na jedno zdarzenie może nasłuchiwać wiele listenerów.
Dodatkowo wspomnę, że na podstawie zdarzeń jesteśmy w stanie odbudować wiedzę na temat tego, co i kiedy stało się w systemie. Ta wiedza, odpowiednio zalogowana, powinna służyć nam szczególnie w znajdywaniu bugów produkcyjnych. I jest bardzo nieoceniona, kiedy pracujemy w systemie rozproszonym.
Połączenie komend ze zdarzeniami
Bardzo częstą praktyką jest łącznie ze sobą architektury CQRS oraz Event Driven (sterowanej zdarzeniami). Łączenie polega na tym, że zdarzenie jest wysyłane na szynę z poziomu Command Handlera. Następnie uruchamiany jest zespół Event Listenerów, które mają za zadanie zwalidować, co się stało oraz (jeżeli to istotne) wysłać nową komendę na szynę. Nie muszę chyba mówić, że to wszystko oczywiście JEST BARDZO ROZSZERZALNE 🙂 .
Maszyna Stanów
Ostatnim, o czym dzisiaj wspomnę jest wzorzec Maszyny Stanów, który (podobnie jak poprzednie) został przeze mnie obszernie opisany w osobnym wpisie. Z grubsza, mamy wzorzec, który działa na poziomie obiektów domenowych i pozwala na definiowanie tranzycji pomiędzy stanami, do których możemy podpinać tzw. callbacki, czyli punkty zawierające logikę biznesową.
Jeżeli do tematu podejdziemy bardzo obiektowo, to rozszerzanie systemu rozpocznie się już od samego początku, kiedy będziemy definiowali stany oraz tranzycje między nimi. W końcu, nowa tranzycja może być osobną klasą, która definiuje stan początkowy oraz końcowy, w obrębie których działa. Niestety, bardzo dużo tutaj zależy od implementacji. Takie rozwiązanie poza byciem (nieco na siłę) zgodnym z OCP, ma swoje wady. Tą największą moim zdaniem jest niemożność rzucenia okiem na wszystkie tranzycje jednocześnie. Dlatego, tego typu rzeczy często kończą się na poziomie pliku konfiguracyjnego.
O ile pchanie na siłę Open-Close Principle do definiowania tranzycji jest słabe, to możliwość podłączania callbacków już jest bardzo mile widziane. I to właśnie z tego powodu zdecydowałem się wliczyć Maszynę Stanów do grona wzorców przyjaznych Open-Close Principle, zamykając tym samym całą listę.
PS. Nie macie wrażenia, że dziś pisałem tylko o listenerach? 😀
Comments are closed.