Separacja domeny od infrastruktury

Wstęp

Obecnie podstawowym wzorcem architektonicznym dla systemów backendowych jest z pewnością MVC i jego pochodne. Pozwala on odseparować w kodzie obszary odpowiedzialne za interfejs użytkownika, model i logikę. Taka struktura ułatwia zarządzanie projektem i pracę programistów z różnych obszarów. Na system można jednak spojrzeć również z innej perspektywy. Większość systemów komputerowych istnieje w celu odwzorowania i automatyzowania różnego rodzaju procesów biznesowych. Te procesy na ogół istniały i są w stanie istnieć samodzielnie. Na przykład biblioteki są w stanie prowadzić proces wypożyczania książek bez wsparcia systemów informatycznych. Te właśnie procesy i zadania biznesowe, które modelujemy w systemie nazywamy domeną lub dziedziną. Niestety wzorzec MVC nie nie jest zaprojektowany z myślą o tej perspektywie i prowadzi do splątania kodu związanego z domeną z kodem służącym obsługującym techniczne szczegóły tego procesu.

Po co separować domenę?

Co możemy zyskać poprzez próby rozplątania tych dwóch obszarów? Po pierwsze zmienia cały nasz sposób myślenia o systemie. Jesteśmy w stanie zastanawiać się nad rozwiązywaniem problemów biznesowych w oderwaniu od ich technicznej realizacji. Zmniejsza to nasze obciążenie poznawcze, pozwala dzielić problem na mniejsze fragmenty i prowadzi do lepszej efektywności.

Jeżeli nauczymy się separować dziedzinę, może nam to również bardzo pomóc w pracy z naszym biznesem. Możemy dzięki temu nauczyć się myślenia od rozwiązywanych problemach w oderwaniu od technicznych pojęć i żargonu. Jesteśmy w stanie się uwolnić ze sposobu myślenia typu 'database driven design’ gdzie względy techniczne decydują o sposobie zamodelowania problemu biznesowego. Zamiast tego możemy sprawniej rozmawiać z biznesem w oparciu o pojęcia domenowe które są im bliskie i znane. Biznesu raczej nie interesuje czy wypożyczenie książki jest modelowane za pomocą rekordu w bazie danych czy dokumentu json.

Znacznie poprawia to również możliwości testowania naszego rozwiązania. Wydzielenie warstw odpowiedzialnych za kwestie techniczne pozwala na ich zamockowanie w testach i sprawdzanie rozwiązań problemów domenowych w izolacji. Możemy na przykład stworzyć sztuczną reprezentację repozytorium obiektów w pamięci i pobierać z niego przewidywalne dane na potrzeby testów. Zyskujemy w ten sposób pewność że problem z połączeniem z bazą danych nie spowodują błędu testu.

Ponadto daje nam to możliwość wymieniania różnych technicznych komponentów systemu. Jeżeli nasz kod domenowy jest odseparowany od sposobu w jaki przechowywane są dane, możemy stosunkowo łatwo wymienić całą warstwę odpowiadającą za utrwalanie danych. Na przykład, na początku przechowywaliśmy dane w formie plików, a później zdecydowaliśmy że potrzebujemy cech zapewnianych przez relacyjne bazy danych, nie musimy przepisywać całego systemy a tylko wydzielone komponenty odpowiedzialne za te zadania. W podobny sposób jesteśmy w stanie wymieniać również inne komponenty, api czy cały główny framework. Oczywiście to bardzo ekstremalne scenariusze, ale już dużo częstszy przypadek aktualizacji wersji frameworka pozwala nam tu dostrzec zalety tego rozwiązania. Znacznie ograniczamy liczbę i miejsca koniecznych zmian i jednocześnie wiemy, że wprowadzone zmiany nie zepsują żadnych funkcji biznesowych.

Jak separować domenę?

Aby przedstawić w jaki sposób doprowadzić do separacji domeny w kodzie oprę się na dwóch chyba najpopularniejszych wzorcach architektonicznych proponujących jak rozwiązać ten problem. Jest to Czysta Architektura przedstawiona przez Roberta C. Martina i Architektura Heksagonalna zaproponowana przez Alistaira Cockburna (nazywana również architekturą portów/adapterów). Nie mam zamiaru tutaj w pełni wyjaśniać szczegółów związanych z tymi rozwiązaniami. Postaram się pokazać natomiast główne koncepty które za nimi stoją oraz jak można podejść do ich implementacji na przykładzie Symfony.

Oba wspomniane wzorce zakładają wydzielenie trzech koncentrycznych warstw, z których każda może zawierać zależności tylko do warstw znajdujących się wewnątrz. Warstwa domeny znajduje się w najbardziej wewnętrznym okręgu. Oznacza to że komponenty domenowe mogą zależeć tylko od innych elementów warstwy domeny, ale nie od żadnych komponentów znajdujących się w warstwach zewnętrznych.

W jaki sposób uzyskać w takim razie dostęp np. do bazy danych? Z pomocą przychodzi nam tu reguła D z zestawu SOLID czyli zasada odwracania zależności (dependency inversion principle). Mówi ona że nasze komponenty nie powinny zależeć od konkretów (konkretnych implementacji klas) a od abstrakcji (interfejsów, klas abstrakcyjnych). Jak to się przekłada na kod? Kontynuując przykład dostępu do bazy danych, w warstwie domenowej możemy definiować interfejs repozytorium dla potrzebnej encji. Zawierał on będzie deklaracje metod z których będziemy chcieli skorzystać do pobrania potrzebnych obiektów. Następnie w warstwie infrastruktury zdefiniujemy klasę będącą implementacją tego interfejsu. Będzie ona realizować dostęp do danych za pomocą wykorzystywanego przez nas mechanizmu, np. Doctrine. Powinniśmy zadbać również o to aby sygnatury argumentów i obiektów zwracanych metod w naszym interfejsie nie wskazywały na klasy spoza warstwy domeny. Na przykład dla kolekcji encji możemy podać wbudowany typ zwracany iterable zamiast ArrayCollection z Doctrine.

Bardziej zewnętrzną warstwą wobec domeny jest warstwa aplikacji. Jej zadaniem jest realizowanie konkretnych przypadków użycia czy komend (odzwierciedlających konkretne procesy biznesowe) systemów z użyciem komponentów z warstwy domeny. Przykładami takich komend dla naszej przykładowej domeny bibliotecznej może być na przykład 'wypożycz książkę’ lub 'nalicz karę’. Będzie to na ogół wymagało skoordynowania działania kilku elementów z warstwy biznesowej.

Najbardziej zewnętrzną warstwą jest infrastruktura zawierająca wszelki kod techniczny oraz implementacje interfejsów z warstw głębszych. Tu znajdą się kontrolery frameworka, wywołania API, implementacje interfejsów i inne tego typu komponenty.

Jak to zaimplementować?

Taka konstrukcja projektu wymaga od nas odrobiny dodatkowej pracy przy konfiguracji projektu w Symfony. Musimy utworzyć niestandardową strukturę katalogów, która będzie odzwierciedlać nasze założenia co do architektury. Dlatego na głównym poziomie w katalogu src znajdą się katalogi Application, Domain. Dopiero w Domain znajdziemy katalog Entity, Repository z definicjami interfesów repozytoriów i resztę elementów pozwalających nam modelować domenę. Natomiast reszta katalogów odpowiadających zadaniom związanych z infrastrukturą jak kontrolery może zostać na domyślnym poziomie bądź również zostać przeniesiona do katalogu Infrastructure również utworzonego na najwyższym poziomie drzewa katalogów.

struktura katalogów projektu

Wskazujemy Symfony gdzie powinno szukać kontrolerów:

konfiguracja routingu
konfiguracja adnotacji

Wskazujemy Doctrine gdzie powinien szukać encji:

konfiguracja Doctrine

Powinniśmy również zadbać o wyłączenie nowego położenia encji z ładowania jako serwisy:

konfiguracja serwisów

I jesteśmy gotowi do pracy.

Linki

https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

https://www.amazon.com/Clean-Architecture-Craftsmans-Software-Structure/dp/0134494164

https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)

Mój poprzedni wpis dotyczący tematu architektury systemu poświęcony modularnym monolitom: http://adamzajac.info/2021/08/06/modularny-monolit-w-symfony/

Subscribe
Powiadom o
guest
0 komentarzy
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x