Każdy komercyjny system, który odniósł biznesowy sukces, kiedyś rośnie. Dokładamy kolejne dziesiątki plików źródłowych, które następnie ze sobą związujemy różnymi metodami, takimi jak na przykład kontener Dependency Injection. Istnieje jednak jedna, bardzo popularna pułapka, która z czasem powoduje zmianę aplikacji w ogromnego kolosa, z którym jest ciężko pracować; jest to brak jakichkolwiek warstw w systemie, nazywany przeze mnie architekturą płaską.

Geneza architektury płaskiej

Większość z nas na początku swojej kariery na pewno próbowała tworzyć system bez jakiegokolwiek frameworka MVC. Takie ćwiczenie z jednej strony jest mało eleganckie, ponieważ często próbujemy odkrywać koło na nowo. Kod takiej aplikacji bywa trudny, zawiły. Jest jednak jedna zaleta takiego rozwiązania: kreatywność tworzenia. Konstruujemy dużą ilość klas, które próbujemy poukładać, tworząc łatwą dla nas architekturę, która pozwoli nam łatwo odnajdywać się w kodzie.

Następnym krokiem w karierze staje się poznanie frameworka, gdzie mamy różnoraką ilość controllerów, resolverów, handlerów czy loggerów. Zauważamy, że bardzo fajnie jest nazywać klasy zgodnie z tym, co w środku robią. Nasze klasy układamy w katalogi takie jak: Entity, Resolver, Authenticator, Factory, Generator, Handler czy EventListener. Z czasem dokładamy kolejne katalogi: Persister, Repository, Subscriber, View oraz ViewModel. Z czasem w każdym z powyższych katalogów zaczyna znajdować się liczba klas nie mniejsza niż 20. Budujemy sobie takiego potworka, który z jednej strony jest fajny, bo bardzo fajnie dokłada się kolejne klasy. Z drugiej strony, praca na takim systemie zaczyna być uporczywa, bo… No właśnie, przejdźmy przez kilka problemów, które z czasem biorą górę nad wszystkim.

Podstawowy problem z fabrykami

Jednym z pierwszych problemów, z jakimi się spotkamy, jest mieszanie teoretycznie tego samego typu klasy, lecz o innym kontekście. Przykładowo, możemy mieć w systemie dwie fabryki: tworzącą instancję clienta do systemu API oraz tworzącą encję. Dorzućmy do tego trzecią faktorkę do tworzenia klasy ViewModelu, który gdzieś dalej będzie serializowany do postaci JSONa jako odpowiedź naszego API.

Wszystkie te klasy powinny zostać wrzucone do katalogu Factory. Przy dobrym wietrze, wewnątrz tego katalogu posegregujemy sobie klasy na podkatalogi: ApiClient, Entity oraz ViewModel. Ale już teraz możemy zauważyć, że coś tutaj jest nie tak: każda z tych klas jest, jak to się potocznie mówi, z innej parafii.

Duże kontrolery robiące wszystko

Drugim potencjalnym dla nas problemem związanym z brakiem świadomości o architekturze warstwowej jest tworzenie dużych serwisów, które robią dosłownie wszystko. Z jednej strony często spotyka się masę logiki biznesowej wewnątrz kontrolerów. Z drugiej strony, wydzielanie takiej logiki do osobnego serwisu nie rozwiąże sprawy, ponieważ zamiast puchnięcia kontrolerów będą puchły nam serwisy, które będą równocześnie dbały o transakcyjność (o czym nieco później), spójność logiki biznesowej oraz kontakt z zewnętrznymi API. Nie mówiąc już o dziesiątej wielokrotności warunków i prywatnych metod.

Zaraz, gdzie ja dokładnie jestem? Co ja tutaj robię?

Przez bajorko wspomniane powyżej ciężko będzie jednoznacznie powiedzieć, na co dokładnie mam wpływ w miejscu, w którym obecnie się znajduję. Bardzo ciężko będzie nam dojść do tego, dlaczego w jednym endpoincie metoda flush() klasy EntityManager wykonuje się pietnaście razy. Do tego, jeżeli przyjdzie nam walczyć z wąskim gardłem endpointu, to długo nam zejdzie nad tym, aby dowiedzieć się, gdzie leci request do zewnętrznego systemu. Nie mówiąc już o koszmarnej liczbie scenariuszy, które musimy pokryć, aby „próbować” odpowiedzieć sobie na pytanie, „co tu właściwie się dzieje?”

Dodatkowo, grzebiąc w implementacji klasy, nie mamy do końca świadomości, w jakiej części systemu obecnie się znajdujemy. Przestajemy wiedzieć, co w aplikacji może popsuć się, jeżeli czegoś w obecnym miejscu nie weźmiemy pod uwagę. Powoli tracimy kontrolę nad systemem, budując sobie coraz bogatszą liczbę bugów znalezionych przez klienta.

Na szczęście nic nie jest stracone, możemy próbować zrefaktoryzować nasz kod, aby móc nad nim zapanować 🙂

Architektura warstwowa w programowaniu domenowym

Propozycja architektury warstwowej rodem z Domain Driven Design jest konceptem starym, o którym niestety ciągle ma świadomość zbyt mała liczba programistów. Jest to koncept, który kroi aplikację na części, w których możemy pozwolić sobie tylko na konkretnego rodzaju operacje. I to dzięki temu właśnie wiemy, gdzie mniej więcej na co możemy liczyć.

Warstwa Domeny

Najpiękniejszą warstwą w całej aplikacji jest domena. Tutaj pozwalamy sobie jedynie na interakcję między klasami, które realizują konkretne zagadnienie biznesowe. Operujemy tutaj na encjach (już wczytanych z repozytoriów!) oraz obiektach wartości. Dla obszerniejszej logiki biznesowej tworzymy klasy serwisów, które operują na wcześniej wspomnianych klasach. W tej warstwie znajdują się również agregaty, które są specjalnego rodzaju serwisami znanymi z DDD. Warstwa domeny na zewnątrz udostępnia klasy eventów, z których może skorzystać aplikacja.

Warstwa domeny jest tą, która nic nie wie o systemach zewnętrznych ani o pozostałych warstwach w systemie.

Warstwa Infrastruktury

Bardzo ciekawą warstwą jest infrastuktura. Tutaj naszym zadaniem jest komunikacja z wszystkim, co jest na zewnątrz naszej aplikacji: od systemu plików, przez systemy bazodanowe, cache czy indeksery, aż do API zewnętrznych usług. Warstwa infrastruktury bardzo często implementowana jest w parze z wzorcem architektury heksagonalnej (nazywanej również architekturą portów i adapterów).

W tej warstwie będziemy mieli przede wszystkim repozytoria, persistery oraz klientów API zewnętrznych usług. Wszystkie serwisy wewnątrz infrastruktury powinny pełnić konkretne zadanie związane z komunikacją na zewnątrz. Nic nie stoi jednak na przeszkodzie, aby wewnątrz niej utworzyć modele, które nieco uproszczą nam przekazywanie danych między serwisami. Pamiętać należy jednak, że będą to modele infrastrukturalne, które nie powinny zbytnio przenikać do żadnej warstwy. To, co szczególne, to nie powinno się ich zaliczać w żaden sposób do warstwy domenowej. Infrastruktura bowiem nie powinna wiedzieć nic o warstwie domeny.

Warstwa Aplikacji

Aplikacja otacza domenę, dostarczając jej wszystkich informacji potrzebnych do realizacji zadania biznesowego. Dodatkowo, jeżeli po realizacji zagadnienia biznesowego jest potrzeba komunikacji z systemem zewnętrznym – również jest to realizowane przez warstwę aplikacji. W tym miejscu również wspomnę o architekturze heksagonalnej – dobrze by było, aby warstwa aplikacji nie miała wiedzy o implementacjach wewnątrz warstwy infrastruktury. W idealnym systemie, warstwa aplikacji powinna móc korzystać z interfejsów (portów) infrastruktury, która sama realizuje swoje zadanie dalej (poprzez odpowiedni adapter).

W warstwie aplikacji będziemy mieli wszelakiej maści komendy oraz handlery. Tutaj będziemy mieli również listenery / subscribery nasłuchujące na eventy wyrzucone przez domenę. W tym miejscu również będziemy mieli zapytania. W sumie, możemy stwierdzić, że warstwa aplikacji powinna realizować takie wzorce jak CQRS oraz Architekturę Event-Driven. Dodatkowo, to warstwa aplikacji jest odpowiedzialna również za transakcyjność (finalnie realizowaną przez infrastrukturę). Czyli to tutaj będziemy robili flusha 😉

W tym miejscu pozwolę sobie jeszcze raz, tym razem jawnie stwierdzić, że warstwa aplikacji powinna wiedzieć o istnieniu domeny, jednakże domena nie powinna nic wiedzieć o aplikacji, która na niej operuje.

Warstwa Interfejsu Użytkownika

Ostatnią, najbardziej oczywistą warstwą jest interfejs, użytkownika końcowego, który uruchamia aplikację. W kontekście aplikacji biznesowych opartych o przeglądarkę tradycyjnie są to kontekst HTTP oraz kontekst CLI.

To tutaj będziemy mieli kontrolery, serializery żądania/odpowiedzi. Tutaj będziemy mieli również klasy typu ViewModel. Jednym zdaniem: wszystko, co realizuje zadanie przetworzenia żądania (HTTP / CLI) na odpowiedź, będziemy mieli w tym właśnie miejscu.

Wiedza warstwy interfejsu użytkownika powinna w sumie kończyć się na wszytkim, co jest jej potrzebne do realizacji CQRS. Czyli będziemy potrzebowali tutaj komend oraz zapytań.

Zadbajmy o dobór odpowiedniej architektury, ale…

Na koniec pragnę zachęcić do zapoznania się z architekturą warstwową na swoim prywatnym projekcie, aby móc poznać jej moc. Dopiero, jak będziemy pewni tego, co chcemy osiągnąć, próbujmy ją wdrożyć. Dokładne zrozumienie tych wszystkich zależności potrafi zabrać trochę czasu oraz kontuzji w projekcie 😀

Nie polecam więc nagłej zmiany architektury w aplikacji produkcyjnej ze względu na hype. Polecam zmianę architektury wtedy, kiedy wiemy, że to co chcemy zrobić ma sens.

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.