Kontroler zamówień zapisuje dane przez Entity Framework Core, a po chwili ta sama reguła biznesowa ma obsłużyć komunikat z kolejki Azure Service Bus. Architektura heksagonalna pozwala oddzielić tę regułę od obu sposobów komunikacji, o ile granica między aplikacją a infrastrukturą nie pozostanie tylko nazwą folderu.

Reguły aplikacji powinny przetrwać wymianę technologii i kanału wejścia
- Sześciokąt to symbol wielu możliwych połączeń, a nie wymóg sześciu portów.
- Port opisuje potrzebę lub operację aplikacji; adapter tłumaczy ją na konkretną technologię.
- Kontroler i konsument kolejki mogą uruchamiać ten sam przypadek użycia.
- Interfejs ma sens, gdy chroni granicę, którą da się wskazać, przetestować lub wymienić.
- Testy jednostkowe i integracyjne sprawdzają różne rzeczy, więc sama liczba atrap nie świadczy o dobrej architekturze.
Sześciokąt nie wymaga sześciu portów
Na schemacie widać figurę, więc łatwo uznać, że liczba boków coś narzuca. Nie narzuca: sześciokąt jest umownym obrazem aplikacji, która może komunikować się z różnymi aktorami i systemami zewnętrznymi.
Alistair Cockburn opisał podejście Ports and Adapters w raporcie technicznym z 2005 roku. Chodziło o to, by aplikację można było uruchamiać z różnymi wejściami i rozwijać bez uzależniania jej reguł od bazy czy urządzenia. Port wyznacza sposób rozmowy z aplikacją, a adapter przekłada ten sposób na konkretny kanał lub technologię.
To rozróżnienie zmienia sposób czytania diagramu. Jeśli na szkicu jest osiem adapterów, nie oznacza to błędu, a jeśli jest jeden interfejs, sama jego obecność jeszcze nie dowodzi, że granica została dobrze zaprojektowana.
Kontroler ASP.NET Core i konsument kolejki mogą wywołać ten sam port
Wyobraź sobie składanie zamówienia: klient wysyła żądanie HTTP, a system magazynowy może później przekazać podobne polecenie przez kolejkę. Oba wejścia powinny uruchamiać ten sam przypadek użycia, zamiast powielać reguły w kontrolerze i obsłudze komunikatu.
W .NET kontroler ASP.NET Core może być adapterem wejściowym, a konsument Azure Service Bus drugim. Port wejściowy, na przykład IPlaceOrder, opisuje operację aplikacji. Wewnątrz przypadek użycia korzysta z portu wyjściowego, takiego jak IOrderRepository; adapter oparty na EF Core realizuje go dla bazy danych.
| Element | Rola w przykładzie | Co można wymienić |
|---|---|---|
| Kontroler HTTP | Odczytuje żądanie i wywołuje port wejściowy | Może go zastąpić lub uzupełnić konsument kolejki |
IPlaceOrder |
Opisuje operację „złóż zamówienie” | Kontrakt pozostaje wspólny dla wejść |
IOrderRepository |
Opisuje zapis lub odczyt potrzebny przypadkowi użycia | Może mieć adapter EF Core albo testową implementację |
| Adapter EF Core | Przekłada port repozytorium na operacje bazy | Może zostać zastąpiony innym sposobem trwałego zapisu |
Ten układ daje prosty sprawdzian projektu: gdy zmieniasz format HTTP albo brokera komunikatów, czy trzeba ruszać reguły składania zamówienia? Jeśli tak, adapter przepuszcza szczegóły transportu do środka. Wspólny przypadek użycia jest tu cenniejszy niż wspólny kontroler, bo drugi kanał może mieć inne ograniczenia dotyczące ponowień, autoryzacji i błędów.
Twórz port wtedy, gdy potrafisz wskazać chronioną granicę
Każde nowe repozytorium z interfejsem może wyglądać na krok ku porządkowi, ale nadmiar abstrakcji tworzy dodatkowe nazwy i przejścia, których nikt nie potrzebuje. Port dodawaj wtedy, gdy izoluje potrzebę aplikacji od konkretnej technologii albo pozwala niezależnie uruchomić przypadek użycia.
Praktyczna reguła jest krótka: jeśli implementacja ma realną szansę się zmienić, istnieje drugi kanał wejścia albo potrzebujesz testować przypadek użycia bez infrastruktury, port zwykle ma uzasadnienie. Gdy interfejs tylko odbija metodę jednej klasy, a obie rzeczy zmieniają się razem, zostaw prostszy kod i wróć do abstrakcji, kiedy pojawi się wyraźna granica.
| Sytuacja | Prawdopodobny skutek | Co zrobić |
|---|---|---|
| Interfejs ma jedną implementację i kopiuje jej metody | Więcej plików bez odseparowania decyzji | Usuń warstwę pośrednią albo nadaj portowi kontrakt opisujący potrzebę aplikacji |
Kontroler bezpośrednio wywołuje DbContext i rozstrzyga reguły zamówienia |
Zmiana wejścia wymaga przenoszenia logiki | Przenieś decyzje biznesowe do przypadku użycia, a zapis za port wyjściowy |
| Test przypadku użycia wymaga uruchomienia SQL Server | Test sprawdza naraz regułę i konfigurację infrastruktury | Podstaw prostą implementację portu w teście jednostkowym, a zapis sprawdź osobno testem integracyjnym |
Ta reguła chroni przed projektem, w którym każda klasa ma interfejs tylko dlatego, że „tak robi czysta architektura”. Interfejs jest użyteczny, kiedy pozwala podjąć niezależną decyzję; sam w sobie nie czyni kodu bardziej heksagonalnym.
Testuj reguły osobno, a połączenia z infrastrukturą razem
Łatwo wpaść w pułapkę: skoro porty ułatwiają podstawianie atrap, wszystkie testy zaczynają sprawdzać współpracę z atrapami. Wtedy test potwierdza umowę z własnym mockiem, ale może przeoczyć błąd mapowania encji, konfiguracji EF Core albo integracji z brokerem.
Reguły domenowe testuj bezpośrednio, a przypadki użycia z prostymi implementacjami portów. Testy integracyjne zostaw dla wybranych połączeń, które muszą działać w rzeczywistym zestawieniu. Microsoft Learn rozróżnia test jednostkowy, który izoluje mały fragment kodu, od integracyjnego, który sprawdza współpracę komponentów, często razem z infrastrukturą.
Przykład dla zamówienia: test jednostkowy sprawdza, czy przypadek użycia odrzuca niedozwoloną zmianę statusu; test adaptera weryfikuje zapis przez EF Core; test API przechodzi przez aplikację testową ASP.NET Core i sprawdza, czy żądanie dociera do właściwego zachowania. Nie trzeba uruchamiać bazy dla każdej kombinacji reguł, ale trzeba choć raz sprawdzić, czy adapter naprawdę zapisuje dane w oczekiwanym kształcie.
Uważam, że najlepszy zestaw testów nie maksymalizuje liczby atrap ani testów integracyjnych. Dzieli odpowiedzialność tak, by szybki test wykrył błąd reguły, a węższy test infrastruktury błąd połączenia.
Podobieństwo do Clean Architecture nie oznacza identycznego schematu
Nazwy potrafią zaciemnić sprawę. Microsoft Learn opisuje Hexagonal, Ports and Adapters, Onion oraz Clean Architecture jako nazwy podobnych układów, w których zależności infrastruktury kierują się do Application Core. To użyteczne przybliżenie, ale nie oznacza, że każdy projekt musi mieć taki sam podział na warstwy i projekty.
W architekturze heksagonalnej myślenie zaczyna się od aplikacji i jej portów: kto ją wywołuje, czego potrzebuje i jakie adaptery to realizują? W Clean Architecture często mocniej wybrzmiewa podział kodu na warstwy i zasada kierunku zależności. W praktyce oba podejścia mogą prowadzić do podobnego kodu .NET, więc nazwa folderu nie rozstrzyga, czy granica jest poprawna.
Gdyby zespół dopiero wydzielał moduł zamówień z dużej aplikacji, postawiłbym na porty nazwane językiem przypadku użycia, takie jak IPlaceOrder, i niewielką liczbę adapterów. Pełny zestaw projektów warstwowych wprowadzałbym dopiero wtedy, gdy odrębne zespoły, kanały lub infrastruktura rzeczywiście potrzebują tych granic. Struktura kodu powinna podążać za zmianami, które system już przechodzi, a nie za diagramem.
Zacznij od jednej reguły, którą chcesz ochronić
Nie trzeba przebudowywać całej aplikacji, by skorzystać z tego wzorca. Wybierz przypadek użycia, którego reguły dziś przeciekają do kontrolera lub kodu dostępu do danych, i wydziel tylko te porty, które pozwolą uruchomić go niezależnie.
Jeśli po zmianie kanału wejścia logika zamówienia pozostaje taka sama, a po podmianie bazy nie trzeba poprawiać jej reguł, granica spełnia swoje zadanie. Od takiego małego sprawdzianu zacząłbym każdą decyzję o szerszym wdrożeniu architektury portów i adapterów.
