Architektura Porty i Adaptery – co i kiedy, a nie jak

Architektura Porty i Adaptery - diagram

Nowy moduł, nowy mikroserwis, czy w ogóle nowy projekt – każda z tych sytuacji wymaga podjęcia wielu decyzji. Jedną z nich będzie wybór odpowiedniej (yhym, taa) architektury. Podejściem, które na pewno warto wziąć pod uwagę jest architektura Porty i Adaptery – nazywana też czasem po staropolsku architekturą heksagonalną. W moim odczuciu, jest to jedno z bardziej uniwersalnych i kompletnych rozwiązań.

Wybór architektury

Jak dobrze wybrać architekturę? Ciężko jednoznacznie odpowiedzieć na to pytanie, ale na pewno istotną kwestią jest właściwy moment. Generalna zasada jest taka, że decyzję powinno się odkładać w czasie możliwie jak najdłużej. Dlaczego? Im późniejsza faza projektu tym ma się więcej informacji, a tym samym prawdopodobieństwo podjęcia właściwej decyzji wzrasta. W przypadku wyboru architektury, według mnie, reguła ta nie oddaje.

  • Za co odpowiedzialny jest ten kod?
  • Co z czym gada?
  • Jakie są zależności?
  • Dlaczego w projekcie te same rzeczy są robione na piętnaście sposobów?
  • Gdzie umieścić kolejny fragment kodu?

Jeżeli pojawiają się takie pytania, to decyzja o architekturze już dawno odjechała. W mojej opinii, architekturę należy zdefiniować przed rozpoczęciem kodowania. Zgodzę się, że w początkowej fazie, kiedy kodu jest stosunkowo niewiele, nie ma to aż takiego znaczenia. Na tym etapie łatwiej będzie wprowadzać zmiany, a też małe projekty z założenia łatwiej ogarnąć. Im później ta praca zostanie wykonana, tym trudniej będzie się wycofać albo dokonać zmiany. Dobra, to kiedy i jaką architekturę wybrać zostawiam Wam. Ja natomiast przychodzę do Was z konkretną opcją, którą warto rozważyć przy podejmowaniu tej decyzji.

Architektury aplikacyjne

Padło kilka razy słowo architektura. Nie umiem w definicje, ale w dużym skrócie:

Architektura jest sposobem organizacji systemu, jego komponentów, powiązań między nimi oraz zestawem reguł, których należy się trzymać przy budowie i rozwoju projektu.

Wyróżniam tutaj pięć popularnych opcji jeżeli chodzi o style architektury aplikacyjnej, bo o takich będę mówił: warstwowa, porty i adaptery, potoki i filtry oraz mikrojądro. W zasadzie w tej prezentacji, oprę się o dwie pierwsze. Spokojnie, potrafię liczyć, Powiedziałem, że jest ich sześć… yyy pięć. Nie wspomniałem o najczęściej wykorzystywanej, czyli architekturze nieokreślonej. Ona zawsze jakaś jest. No właśnie – jakaś. Brak zdefiniowanej architektury, to też architektura.

Gdybym miał podsumować ją jednym zdaniem to powiedziałbym, cytując za rapowym klasykiem, że jest jak nordic walking – nikt nie wie o co chodzi, ale wszyscy w to idą. Swoją drogą kojarzycie z jakiego kultowego serialu jest powyższe zdjęcie? The walking dead.

Porty i Adaptery

Architektura Ports & Adapters zakłada, że rdzeń systemu jest centralnym punktem izolowanym od świata zewnętrznego. Jedyna komunikacja ma miejsce przez kontrolowane porty. Port to punkt wejścia albo wyjścia. W wielu językach, w tym PHP, będzie to po prostu interfejs. Adapter to klasa, która spełni owy port. Istnieją dwa rodzaje adapterów: primary (zwane też driving lub incoming w zależności od przyjętej konwencji nazewniczej) oraz secondary (zwane też odpowiednio driven lub outgoing). Mówiąc po polsku, te pierwsze sterują aplikacją, a te drugie są sterowane przez aplikację.

Adapter typu primary jest zależny od portu. W tym wypadku port jak i implementacja należą do aplikacji. Za to adapter typu secondary jest konkretną implementacją portu. Jest to znany i lubiany wzorzec projektowy Adapter. Wówczas port należy do aplikacji, ale implementacja pochodzi z zewnątrz. Takie podejście to klasyczne odwrócenie zależności, czyli litera D (dependency inversion) z akronimu SOLID. Dzięki temu aplikacja jest wolna od sprzężeń ze szczegółami implementacyjnymi.

Przykłady sterujących adapterów to kontrolery API, czy komendy CLI. Tak naprawdę wszystkie intencje, które przychodzą z zewnątrz. Najczęściej od użytkownika. Może to być port chociażby na serwis, który jest wstrzykiwany do kontrolera, czy też na szynę komend w przypadku użycia wzorca CQRS. Innymi słowy te adaptery opakowują porty.

Za to sterowane adaptery implementują porty. To będą wszystkie operacje delegowane do zewnętrznych zależności takich. Framework, ORM, biblioteka do brokera wiadomości, ale też mniejsze rzeczy jak generowanie UUID, czy wykorzystanie klasy Money do obsługi operacji na pieniądzach. Wszystko to czego nie ma sensu implementować na własną rękę tylko lepiej skorzystać z gotowych rozwiązań. W myśl zasady, żeby nie wymyślać koła na nowo.

Organizacja katalogów w projekcie

Wiedząc to wszystko, przykładowa organizacja przestrzeni nazw mogłaby wyglądać następująco. Katalog domain, gdzie trafią logika biznesowa i porty. Katalog adapters, gdzie znajdą się już te konkretne implementacje spełniające owe porty.

src
  Domain
	Model
	Ports
  Adapters

Moim zdaniem taki układ się nie sprawdzi. Da się to zrobić czytelniej. Generalnie, idea wzorca Porty i Adaptery nie narzuca, gdzie co umieszczać. Pokazuje wyłącznie, jak komunikować się między dziedziną, a światem zewnętrznym. W takim razie ten podział także się w to wpisuje. Wspominam o tym też dlatego, że nie każdy pracuje na zielonym poletku. W zastanych projektach też można korzystać z dobrodziejstw tej architektury. Właśnie poprzez upraszczanie pewnych podziałów, gdzie domena prawdopodobnie nie będzie w całości izolowana, ale konkretne kawałki jak najbardziej można odseparować. Nie ma jednej drogi, jak realizacja tego konceptu ma wyglądać. Jest to rozwiązanie bardzo elastyczne.

src
  Domain
  Application
  Infrastructure

Dużo częściej, podział wygląda tak. To jest architektura warstwowa. Świetnie się łączy z portami i adapterami. To o czym mówiłem na początku, że będę bazował na tych dwóch. Czasem jeszcze być może traficie na podział z dodatkową warstwą prezentacji (nazywaną też interfejsem użytkownika). Wówczas w warstwie infrastruktury znajdą się adaptery drugorzędne, a w prezentacji pierwszorzędne. Taki podział może mieć sens, ale tak naprawdę w samej przestrzeni infrastruktury też da się to zorganizować. Za to w tych 3 warstwach zależności klarują się dużo lepiej.

src
  Domain
  Application
  Infrastructure
  Presentation

Jak to w architekturze warstwowej, warstwy komunikują się ze sobą w zgodzie z regułami. I w tym wypadku kierunek zależności jest od dołu do góry, czyli infrastruktura może zależeć od aplikacji i dziedziny, aplikacja od dziedziny i dziedzina powinna być niezależna. To w połączeniu z portami i adapterami jest totalnym sztosem. Możecie to spotkać pod różnymi akronimami. Z niewielkimi różnicami, ale ogólnie jest to coś na kształt Domain Architecture, Onion Architecture, czy Clean Architecture. Różnie w literaturze nazywane, różnie pokazywane, ale sprowadza się moim zdaniem do tego samego z różnym wykonaniem.

Zależności w Ports & Adapters

I teraz zdaję sobie sprawę, że niektórzy z Was pomyśleli: „o czym ten typ w ogóle gada”. Jeszcze raz, żeby Wam to lepiej zobrazować. Domena – serce aplikacji. Nazywana też dziedziną, logiką biznesową, czy rzadziej inteligencją. To jest ten fragment systemu, na którym najbardziej trzeba się skupić. To on definiuje biznes i stanowi o unikalności systemu. Dziedzina powinna być możliwie jak najbardziej niezależna.

Architektura Porty i Adaptery - diagram

Domena komunikuje się za pomocą portów. Zielone fragmenty symbolizują porty wystawione na świat, a strzałki to adaptery. Dalej jest warstwa aplikacji odpowiedzialną za orkiestrację domeną. To są przypadki użycia. Techniczny przykład: serwis, który wyciąga obiekt domenowy z repozytorium, wywołuje na nim konkretną operację i utrwala w repozytorium. Ta warstwa także wystawia porty. Ostatnią warstwą jest infrastruktura. Znajdują się w niej adaptery, czyli zewnętrzne zależności. Tutaj także są porty, ale te pierwszorzędne. Mam nadzieję, że to trochę rozjaśniło cały ten przepływ zależności i komunikacji.

Co znajduje się w danej warstwie?

Pamiętam, że kiedy uczyłem się tej architektury to miałem dwa problemy. Pierwszy, jak zrobić żeby nie uzależniać warstw od siebie. I to załatwiają porty i adaptery, co jeszcze pokażę później na przykładzie kodu. Drugi, gdzie umieścić poszczególne byty w tych warstwach. Mam tu małą ściągę, jak mogłoby to wyglądać. Uwaga – nie są to nazwy katalogów tylko lista elementów.

Aggregates
Entities
Value Objects
Services
Factories
Events
Enums
Exceptions
Repositories Ports

W domenie znajdą się takie byty jak: agregaty (jeżeli projekt opiera się o DDD). Dalej encje, wartości, serwisy domenowe, fabryki do konstruowania złożonych obiektów, zdarzenia i inne bardziej techniczne jak enumy, czy wyjątki. Pojawią się też porty. Najczęściej porty repozytoriów i serwisów.

Commands
Handlers
Services
DTOs
Exceptions
Queries Ports
Services Ports

Warstwa aplikacji to komendy i handlery (jeżeli projekt opiera się o CQRS) lub po prostu serwisy aplikacyjne. Warstwa aplikacji to pośrednik między domeną, a infrą. Wylądują tutaj więc takie byty jak proste obiekty do transferowania danych. Jeżeli chodzi o porty to mogą to być zapytania, czy serwisy, które potrzebują czegoś ze świata zewnętrznego.

Controllers
CLI Commands
Services
Repositories
Queries
Exceptions
Requests
Validators

No i infrastruktura – tak naprawdę wszystko pozostałe. Kontrolery i komendy CLI (ewentualnie do prezentacji), serwisy infrastrukturalne, adaptery, repozytoria, zapytania, wyjątki, żądania, walidatory i tego typu bardziej techniczne rzeczy. Te bliżej gruzu.

Porty i Adaptery – przykład w PHP

src
  Recruitment
    Domain
      RecruitmentRepositoryInterface
    Application
    Infrastructure
      RecruitmentRepository

Idąc za ciosem, jak to wygląda w kodzie. Najpierw klasyka – repozytorium pozwalające pobrać i utrwalić obiekt. Port znajduje się w domenie. Za to faktyczna implementacją, która prawdopodobnie będzie zahaczała o bazę danych musi trafić do infry.

<?php
 
declare(strict_types=1);

namespace Koddlo\Recruitment\Domain;

use Koddlo\Recruitment\Domain\RecruitmentNotFoundException;
use Koddlo\Shared\Domain\Id;

interface RecruitmentRepositoryInterface
{
   public function save(Recruitment $recruitment): void;

   /**
    * @throws RecruitmentNotFoundException
    */
   public function get(Id $id): Recruitment;
}

Tak wygląda port w domenie. Dwie metody save do zapisu obiektu rekrutacji i get do jego pobrania. Chyba nic odkrywczego.

<?php
 
declare(strict_types=1);

namespace Koddlo\Recruitment\Infrastructure;

use Doctrine\ORM\EntityManagerInterface;
use Koddlo\Recruitment\Domain\RecruitmentNotFoundException;
use Koddlo\Recruitment\Domain\Recruitment;
use Koddlo\Recruitment\Domain\RecruitmentRepositoryInterface;
use Koddlo\Shared\Domain\Id;

final class RecruitmentRepository implements RecruitmentRepositoryInterface
{
   public function __construct(
       private EntityManagerInterface $entityManager
   ) {}

   public function save(Recruitment $recruitment): void
   {
       $this->entityManager->persist($recruitment);
       $this->entityManager->flush();
   }

   public function get(Id $id): Recruitment
   {
       return $this->entityManager->find(Recruitment::class, $id->toString())
           ?? throw new RecruitmentNotFoundException();
   }
}

Następnie adapter w infrastrukturze. Domena nic nie wie, w jaki sposób jest to realizowane. W tym momencie zapis delegowany jest do Entity Managera z Doctrine ORM. Jutro może być PDO, a po jutrze zmiana z MySQL na MongoDB. A kontrakt w domenie bez zmian. To jest to o czym mówiłem, czyli domena mówi co i kiedy, a infrastruktura jak.

Mała dodatkowa wrzutka. Zauważcie, że metoda get zwraca encję Doctrine. Jest to możliwe, gdyż samo mapowanie kolumn da się wynieść do infrastruktury, używając notacji opartej o XML. Jedyna zależność, której nie da się na ten moment pozbyć to ArrayCollection. Mówię o tym dlatego, bo innym rozwiązaniem jest posiadanie osobnych modeli w domenie i w infrastrukturze, a to wiąże się z tym, że trzeba pisać transformery pozwalające przetłumaczyć te obiekty. Rozwiązanie to jest bardziej elastyczne, ale wymaga więcej pracy. Wtedy, tak naprawdę, domena jest całkowicie niezależna. Tyle, że ORM takie jak Doctrine gwarantują też dużą elastyczność w mapowaniu i da się robić dziedzinę bez wpływu infrastruktury na domenę. Niemniej, przy użyciu innych rozwiązań jak chociażby Eloquent będzie to już prawie niemożliwe Wtedy faktycznie rozsądniej iść w transformacje, która jak mówię, ma dużo plusów i nie wymusza konkretnej metody persystencji. Nie o tym jest ten materiał, ale warto być tego świadomym. Niejednokrotnie warto pójść na kompromisy i podejść pragmatycznie zamiast sztucznie walczyć o zgodność z teorią.

Drugi przykład to generowanie identyfikatora. W tamtym przykładzie pominięta była warstwa aplikacji. Tutaj za to wszystkie trzy warstwy grają role.

src
  Shared
    Domain
      Id
    Application
      IdGeneratorInterface
    Infrastructure
      IdGenerator

Ponownie, najpierw domena. Tym razem obiekt reprezentujący identyfikator domenowy. Nic się tutaj nie dzieje. Value Object w najprostszej postaci.

<?php
 
declare(strict_types=1);

namespace Koddlo\Shared\Domain;

final class Id
{
   public function __construct(
       private string $id
   ) {}

   public function toString(): string
   {
       return $this->id;
   }

   public function equals(self $otherId): bool
   {
       return $this->id === $otherId->id;
   }
}

W warstwie aplikacji port pozwalający wygenerować nowy identyfikator. Tak jak wspomniałem, porty to zwykłe interfejsy. W tym miejscu, dlatego bo często na tym poziomie będzie to robione. Jeżeli z jakiegoś powodu w domenie będą generowane identyfikatory to ten port być może zostałby wyniesiony warstwę wyżej.

<?php
 
declare(strict_types=1);

namespace Koddlo\Shared\Application;

use Koddlo\Shared\Domain\Id;

interface IdGeneratorInterface
{
   public function new(): Id;
}

No i infrastruktura, czyli implementacja korzystająca z biblioteki Ramsey. Jutro można to zmienić na Symfony Uid, a po jutrze zmienić z Uuid v4 na Uuid v7. Zresztą ostatnio to robiłem w projekcie i dokładnie do tego się sprowadzało. Znowu w aplikacji i domenie nic się nie zmieni. Dobra, czyli widzicie, nie jest to wcale taka magia i po zrozumieniu tych reguł jest to całkiem przyjemne w implementacji.

<?php
 
declare(strict_types=1);

namespace Koddlo\Shared\Infrastructure;

use Koddlo\Shared\Application\IdGeneratorInterface;
use Koddlo\Shared\Domain\Id;
use Ramsey\Uuid\Uuid;

final class IdGenerator implements IdGeneratorInterface
{
   public function new(): Id
   {
       return new Id(Uuid::uuid4()->toString());
   }
}

Jak automatycznie kontrolować architekturę?

Kolejna sprawa, to pilnowanie zależności między warstwami. Powinny być one kontrolowane automatycznie. Człowiek jest omylny i nawet, gdy dokładnie stara się ich pilnować to zdarzy się, że coś się prześlizgnie. Po co się męczyć, skoro są do tego narzędzia. Ja znam dwa: Deptrac i PhpAT. Oba dadzą radę, ale ja posiadam większe doświadczenie w tym pierwszym.

parameters:
  paths:
    - ./src
  layers:
    - name: RecruitmentDomain
      collectors:
        - type: className
          regex: Koddlo\\Recruitment\\Domain\\.*
    - name: RecruitmentApplication
      collectors:
        - type: className
          regex: Koddlo\\Recruitment\\Application\\.*
    - name: RecruitmentInfrastructure
      collectors:
        - type: className
          regex: Koddlo\\Recruitment\\Infrastructure\\.*
    - name: Vendor
      collectors:
        - type: bool
          must_not:
            - type: className
              regex: Koddlo\\.*
            - type: classNameRegex
              regex: '#^DateTimeImmutable$|^Exception$|^Throwable$#'
  ruleset:
    PaymentDomain:
    PaymentApplication:
      - PaymentDomain
    PaymentInfrastructure:
      - PaymentDomain
      - PaymentApplication
      - Vendor
    RecruitmentDomain:
    RecruitmentApplication:
      - RecruitmentDomain
    RecruitmentInfrastructure:
      - RecruitmentDomain
      - RecruitmentApplication
      - PaymentInfrastructure
      - Vendor

Ogólnie główna konfiguracja to plik deptrac.yaml, który składa się z trzech sekcji. Paths to ścieżki, które chce się poddać analizie. I tutaj prosto, np. cały katalog src.

Dalej layers, czyli warstwy. To jest miejsce, gdzie zostaną zdefiniowane właśnie trzy podstawowe warstwy: domena, aplikacja i infrastruktura oraz ich przestrzenie nazw. Zauważcie, że jest też warstwa Vendor. Ona reprezentuje wszystko, co pochodzi z zewnątrz. Kolektory określają, co brać pod uwagę jako daną warstwę. Jest to całkiem elastyczne. W tym przykładzie deklaruję, że wszystko co pochodzi z głównej przestrzeni nazw poza DateTimeImmutable, Exception i Throwable ma być traktowane jako Vendor. Zaraz Wam pokażę, po co są te wyjątki.

Ostatnia sekcja to ruleset, czyli reguły. Tym razem definiuje się, co od czego może zależeć. Tak jak mówiłem, domena nie zależy od niczego, aplikacja od domeny i infrastruktura od wszystkich powyższych. Ma także dostęp do vendora, do którego dostępu nie mają aplikacja i domena. Oczywiście, poza tymi trzema wyjątkami, które wcześniej zdefiniowałem. Exception w domenie mnie nie boli. Pewnie będę miał swoje klasy wyjątków, ale będą one dziedziczyły po wyjątkach języka PHP. Tak samo być może będę chciał używać ArrayCollection z Doctrine w domenie. Ogólnie warto takie wyjątki definiować świadomie, ale plus jest taki, że mamy to jasno zadeklarowane.

Dalej, zobaczcie, że są tutaj dwa moduły Payment i Recruitment. Ogólnie przy takiej konfiguracji nie da się uzależnić modułów od siebie. Poza warstwą infrastruktury w module rekrutacji, która może zależeć od infrastruktury modułu płatności, bo zostało to zdefiniowane. Wszystko jasno i na papierze, co od czego zależy. Muszę też wspomnieć przy okazji, że mogą istnieć zależności ukryte. I wyróżniam trzy przypadki:

  1. Wspólna baza danych, gdzie te same dane są wykorzystywane przez wiele modułów.
  2. Kontener DI jest współdzielony i konfiguracja spaja moduły.

Tego typu ukryte zależności często są problematyczne. Normalne, że czasem stosuje się stosuje kompromisy. Po to by coś zyskać, a coś stracić. Ale to już inny temat. Wiem, powiedziałem że są trzy. Jeden wyleciał mi z głowy albo jednak nie potrafię liczyć…

Kończąc już kwestię tego narzędzia, mając taki plik konfiguracyjny można uruchomić weryfikację. Jeżeli znajdą się nadużycia to przykładowe wyjście może wyglądać następująco.

Widać tutaj, że użyto DTO z płatności w domenie rekrutacji. Od razu dam Wam wskazówkę. To powinno normalnie chodzić na pipelinach. Tak jak statyczne analizy kodu i testy. Wtedy nie będzie miejsca na nadużycia, ani zbędne dyskusje podczas recenzowania kodu. Wszystkie wyjątki, na które zespół się zgodzi, będą jawnie wskazane w konfiguracji.

Zalety architektury Porty i Adaptery

OK, powoli zbierając to do kupy. Kilka zalet już padło między wierszami, ale czas to wszystko uporządkować. Zalety:

  • domena w centrum zainteresowań,
  • luźne powiązania,
  • wymienialność,
  • elastyczność,
  • utrzymywalność,
  • skalowalność,
  • testowalność,
  • brak wad.

Główną ideą Portów i Adapterów jest odseparowanie logiki biznesowej od zewnętrznych zależności. Pozwala to faktycznie skupić się na esencji danej aplikacji i zapewnić sobie pełną kontrolę nad domeną.

Normalnie, kiedy bezpośrednio używa się zależności, biblioteki czy komponentu i frywolnie wrzuca ją do całej aplikacji to tak naprawdę jest to zgoda na całkowite uzależnienie. Za to odwrócenie zależności powoduje, że ustalane są reguły gry, a jak coś Ci się nie spodoba to wypad. Z łatwością Cię wymienię na inną bibliotekę. Tak w ogóle, mam wrażenie, rozmawiają nasze systemy. Agresywne skurwiele.

I to jest niesamowite uczucie. Komponenty są luźniej powiązane i łatwo można separować problemy na poziomie architektonicznym. A to też pozwala zarządzać długiem technicznym. Prościej podnosi się wersje zależności, czy wymienia na inną bibliotekę. Nie powiedziałbym, że łatwo, bo to zależy od przypadku, ale na pewno łatwiej.

Architektura heksagonalna skupia się na tym co i kiedy, a nie jak, stąd taki podtytuł tego materiału. Porty gwarantują co i kiedy, a adaptery jak. Tyle, że jak staje się łatwo wymienialne.

Rozwiązanie to daje sporą elastyczność. Można ją zastosować dla bogatego modelu domenowego – rozwiązanie pożądane. Sprawdzi się też dla bardziej prostego, anemicznego modelu. Pytanie, czy faktycznie wtedy jest dobrym wyborem. Według mnie czasem tak, ale pewnie do projektu na za tydzień, który umrze za dwa bym jej nie wrzucał. Tyle, że takie projekty to rzadkość. Częściej występują jakieś PoC (Proof of Concept) albo MVP (Minimum Viable Product). W tym wypadku to można się modlić, żeby mieć tego typu problemy, że trzeba przerabiać architekturę. Będzie to oznaczało, że projekt wypalił. A większość z nich prawdopodobnie będzie do zaorania. W każdym razie, kiedy płynnie poruszacie się po tej architekturze to okazuje się, że ten narzut wcale nie jest taki wielki, a już na pewno nie jest, kiedy pozwolicie sobie na pewne kompromisy. Zawsze warto podchodzić pragmatycznie.

Ogólnie, nie wchodząc w szczegóły, jest to rozwiązanie przyjemne w utrzymaniu, rozwoju i skalowaniu. Bardzo duży nacisk kładzie na łatwe testowanie. Będzie więcej testów jednostkowych, a mniej integracyjnych, a to zazwyczaj dobry układ, bo testy jednostkowe są tanie i szybkie.

Pamiętacie, wspomniałem że wiele decyzji warto odłożyć na później. Architektura heksagonalna jak najbardziej na to pozwala. Bazując na abstrakcji, szczegóły implementacyjne mogą zejść na drugi plan. W przypadku błędnej decyzji, dużo łatwiej będzie to wyprostować.

No i ostatnia zaleta to brak wad. To oczywiście z przymrużeniem oka, ale prawda jest taka, że tych wad za bardzo nie ma. Przynajmniej ja ich nie dostrzegam. Może poza pewnym skomplikowaniem i dodatkowym narzutem, bo oczywiście separacja zagadnień zawsze wiąże się z narzutem. Trzeba będzie działać na abstrakcji albo transformować obiekty z jednej warstwy na drugą i tak dalej. W praktyce gorszy jest brak reguły, który okazuje się często trudniejszy do pojęcia, niż sztywne, nawet skomplikowane, zasady.

Architektura heksagonalna – podsumowanie

Podsumowując, uważam, że jest to najbardziej uniwersalna architektura. Niektórzy powiedzą, że to przeinżynierowanie i na CRUD-a za dużo, ale prawda jest taka, że mało jest systemów, gdzie istnieje sam CRUD. A nawet jeśli, to co, trzeba mieć rozmach. CRUD-a też można robić w elegancki sposób. Tak jak mówię, zazwyczaj jest to trochę CRUD-a, a trochę bardziej złożonych operacji.

Mając dobrze wydzielone moduły, można aplikować różne architektury do różnej złożoności problemu. Tutaj też zawsze pojawia się pytanie, czy lepiej robić homogenicznie, czy może różnorodność jest pożądana. Im więcej podejść w projekcie, tym wyższy próg wejścia, ale dzięki temu zyskuje się rozwiązanie szyte na miarę. Jednak, w praktyce, biorąc pod uwagę wszystkie uwarunkowania, często warto dążyć do spójności.

Życzyłbym sobie, żeby Porty i Adaptery były popularne bardziej, niż to nieszczęsne MVC i jego pochodne, promowane chociażby przez frameworki. Przerobiłem kilka projektów opartych o MVC i za każdym razem efekt był odwrotny, niż po ostrym kebabie. W sensie, dupy nie urwało.

Dobra, wychwaliłem architekturę heksagonalną, ale nie chcę też powiedzieć, że jest to rozwiązanie idealne na wszystkie problemy, bo takie nie istnieją. Ostatecznie dobrze byłoby dobierać architekturę do problemu. Macie swoje mózgi, pewnie lepiej działające od mojego. Sami zdecydujcie, na podstawie tego co powiedziałem, czy będziecie jej używać. Moim zdaniem warto.

Programista PHP i właściciel marki Koddlo. Pasjonat czystego kodu i dobrych praktyk programowania obiektowego. Prywatnie fan dobrego humoru i podcastów.