Czy hexagonal architecture jest naprawdę tak bardzo skomplikowanym i trudnym wzorcem, jak pokazuje to większość poradników w internecie?
Hexagonal (architecture) – skąd się to wzięło?
Porty i adaptery – ports and adapters, bo taka jest druga nazwa tego wzorca wymyślonego przez Alistair-a Cockburn, który szumnie nazywany jest architekturą heksagonalną. Główną przesłanką było tworzenie słabo powiązanych ze sobą komponentów, tak, aby były one łatwo wymienialne na potrzeby testów, jak i potencjalnej podmiany komponentu (np. dostawcy usług auth).
Przez wiele lat narosło wokół tego podejścia wiele mitów, które sprowadziły go na złą drogę… Określanie „architektura” odnoszące się do tego prostego wzorca jest używany zbyt przesadnie, gdyż samo podejście jest bardzo proste. Dodajmy do tego słynny heksagon, który omawiany jest w prawie wszystkich poradnikach w internecie i visible confusion gwarantowane.

https://en.wikipedia.org/wiki/File:Hexagonal_Architecture.svg
O co tu tak naprawdę chodzi?
Zapomnijmy o powyższym obrazku i wyobraźmy sobie, że posiadamy repozytorium, które używa ORM do komunikacji z bazą danych. Jest ono stosowane w klasie serwisowej, która wykonuje pewną logikę i używa repozytorium do interakcji z bazą danych.
Class TagRepository {
async create(entity: TagEntity): Promise<void> {
await orm.insert(entity);
}
async getOne(id: string): Promise<TagEntity | null> {
return await orm.where('id', '=', id)
.firstOrNull();
}
}
class TagsService {
constructor(
private readonly tagRepository: TagRepository
) { }
// logic
}W myśl zasad SOLID, a dokładniej, to SOLID-owego D, powinniśmy utworzyć interfejs, który nasze repozytorium zaimplementuje, tak, aby używając go w serwisie nie mieć mocnego powiązania pomiędzy klasami.
interface TagRepositoryIntreface {
create(entity: TagEntity): Promise<void>;
getOne(id: string): Promise<TagEntity[]>;
}
class TagRepository implements TagRepositoryIntreface {
async create(entity: TagEntity): Promise<void> {
await orm.insert(entity);
}
async getOne(id: string): Promise<TagEntity | null> {
return await orm.where('id', '=', id)
.firstOrNull();
}
}
class TagsService {
constructor(
private readonly tagRepository: TagRepositoryIntreface
) { }
// logic
}Co, jeśli powiem Ci, że prawie zaimplementowałeś/zaimplementowałaś ten słynny hexagon? W tym wzorcu chodzi tylko i wyłącznie o posiadanie luźnych powiązań systemu z elementami, które wychodzą poza jego lokalny kontekst (baza, obsługa plików, http i inne). Daje nam to możliwość łatwego testowania, bo nie musimy używać wtedy magii framework-ów testujących. Wystarczy napisać odpowiedni adapter (o tym zaraz), który nie będzie dotykał tych elementów.
W tym wzorcu nie ma mowy o tym, że katalogi mamy dzielić na app, domain czy infrastructure!
Lecz nie zrozum mnie źle, wzorzec portów i adapterów, to nie to samo, co SOLID-owe D (choć czerpie z niego wielkimi garściami), ale wszelkie wątpliwości rozwiąże przykład.
Przykład hexagonal architecture
Teraz wyobraź sobie, że chcesz szybko ruszyć na produkcję ze swoim systemem, ale nie macie jeszcze w zespole ADR-a (architecture decision record), co do tego na jaką bazę postawicie. Aby nie opóźniać development-u i być w stanie pokazywać klientowi progress, decydujecie się na in-memory storage. Musicie tylko pamiętać, żeby późniejsze podpięcie prawdziwej bazy nie wymagało zmian istniejącego kodu.
interface TagRepository {
create(entity: TagEntity): Promise<void>;
getOne(id: string): Promise<TagEntity[]>;
}
class InMemoryTagRepository implements TagRepository {
constructor(
private tags: Map<string, TagEntity> = new Map();
) { }
async create(entity: TagEntity): Promise<void> {
if (!this.tags.has(entity.id)) {
this.tags.set(entity.id, entity);
}
}
async getOne(id: string): Promise<TagEntity | null> {
return this.tags.get(id);
}
}
class TagsService {
constructor(
private readonly tagRepository: TagRepository
) { }
// logic
}Wasze unit testy mogą teraz lecieć od góry do dołu, po całym komponencie, ponieważ nie dotykają fizycznie bazy danych. Zaś w momencie wejścia na produkcję z prawdziwą bazą, wystarczy na mechanizmie DI napisać regułę, która mówi, że w przypadku aplikacji w stanie produkcyjnym używaj adaptera SqlRepository, zaś na potrzeby testów, istniejącego adaptera InMemory.
class SqlTagRepository implements TagRepository {
async create(entity: TagEntity): Promise<void> {
await orm.insert(entity);
}
async getOne(id: string): Promise<TagEntity | null> {
return await orm.where('id', '=', id)
.firstOrNull();
}
}Tym sposobem interfejs TagRepository jest portem, zaś SqlTagRepository oraz InMemoryTagRepository jego adapterami, które wybierane są automatycznie względem potrzeb aplikacji.
TO WSZYSTKO!
I wpis mógłbym w tym momencie skończyć, jednak chciałbym jeszcze poruszyć jedną kwestię…
App, Domain, Infrastructure
Jak często widujesz poradniki, bądź projekty, które używają warstwowej struktury plików i katalogów i mają przypiętą łatkę hexagonal architecture?
src/
│ ├── module/
│ │ ├── api-rest/
│ │ │ ├── controller.ts
│ │ ├── app/
│ │ │ ├── service.ts
│ │ ├── domain/
│ │ │ ├── aggregate.ts
│ │ ├── infrastructure/
│ │ │ ├── repository.tsLub co gorsza można zauważyć foldery o nazwach ports i adapters… Jak już wspomniałem wyżej, zamysłem tego wzorca jest łatwość pisania testów, a także elastyczność podmiany implementacji, tych części systemu, które są niepewne, bądź chcemy odwlec pewne decyzje.
Zgadzam się co do tego, że połączenie wzorca portów i adapterów z odpowiednio ułożonymi folderami w architekturze warstwowej daje wymierne rezultaty w postaci posiadania czystej struktury plików i katalogów oraz samego kodu. Wtedy port repozytorium z przykładu powyżej może być umieszczony w folderze domain, zaś jego adaptery w infrastrukturze. Takie podejście (połączenie architektury warstwowej ze wzorcem portów i adapterów) często określane jest mianem czystej architektury – clean architecture.
Podsumowanie
- Twórcą ports and adapters jest Alistair Cockburn.
- Hexagon, to wzorzec projektowy, który pozwala pisać łatwo testowalny i wymienialny kod.
- Podejście to nie określa sposobu w jaki należy organizować strukturę plików i folderów swojego projektu.


Dodaj komentarz