← Blog
tutorials

Hexagonálna architektúra | Praktický sprievodca pre moderné aplikácie

Naučte sa, čo je hexagonálna architektúra, prečo je dôležitá a ako ju implementovať. Kompletný sprievodca s reálnymi príkladmi pre budovanie udržateľného, testovateľného softvéru.

Ryveris Team ·
Hexagonálna architektúra | Praktický sprievodca pre moderné aplikácie

Hexagonálna architektúra je návrhový vzor softvéru, ktorý izoluje vašu jadrovú obchodnú logiku od externých systémov ako databázy, API a používateľské rozhrania. Myšlienka je jednoduchá: váš doménový kód by nikdy nemal závisieť na infraštruktúre. Infraštruktúra závisí na doméne.

Vzor predstavil Alistair Cockburn v roku 2005. Môžete ho tiež počuť pod názvom “Ports and Adapters.” Oba názvy opisujú rovnaký koncept.

Prečo existuje

Väčšina aplikácií začína rovnako. Obchodná logika sa zamotá s databázovými dotazmi, HTTP handlermi a volaniami služieb tretích strán. Spočiatku to funguje. Potom kódová základňa rastie a dejú sa tri veci:

  1. Testovanie sa stáva bolestivým. Nemôžete testovať obchodné pravidlá bez spustenia databázy alebo mockovania polovice frameworku.
  2. Zmena infraštruktúry je riziková. Výmena platobného poskytovateľa alebo migrácia z REST na GraphQL znamená prepisovanie obchodnej logiky.
  3. Kód sa ťažko chápe. Nikto nevie povedať, kde doména končí a infraštruktúra začína.

Hexagonálna architektúra rieši všetky tri problémy vynútením jediného pravidla: závislosti vždy smerujú dovnútra.

Základné koncepty

Predstavte si vašu aplikáciu ako tri sústredné vrstvy.

Doména (stred)

Toto je vaša obchodná logika. Čisté funkcie, entity, value objekty a doménové služby. Má nulové závislosti na frameworkoch, databázach alebo externých knižniciach. Hovorí svojím vlastným jazykom.

Pre e-commerce systém táto vrstva obsahuje koncepty ako Order, Product, PricingRule a InventoryPolicy. Tieto objekty nevedia nič o SQL, HTTP ani JSON.

Porty (stredná vrstva)

Porty sú rozhrania, ktoré definujú, ako vonkajší svet interaguje s doménou. Sú to kontrakty, nie implementácie.

Existujú dva typy:

  • Vstupné porty (riadiace). Definujú, čo aplikácia dokáže robiť. Predstavte si ich ako prípady použitia. “Zadať objednávku.” “Zrušiť predplatné.” “Vygenerovať faktúru.”
  • Výstupné porty (riadené). Definujú, čo aplikácia potrebuje od vonkajšieho sveta. “Uložiť objednávku.” “Odoslať notifikáciu.” “Získať výmenný kurz.”

Porty sú súčasťou doménovej vrstvy. Sú písané v doménovom jazyku, nie v jazyku infraštruktúry. Píšete OrderRepository, nie PostgresOrderDAO.

Adaptéry (vonkajšia vrstva)

Adaptéry sú konkrétne implementácie, ktoré prepájajú reálny svet s vašimi portami.

  • Vstupné adaptéry prekladajú externé požiadavky na doménové volania. REST kontrolér, GraphQL resolver, CLI príkaz, konzument fronty správ. Všetko toto sú vstupné adaptéry.
  • Výstupné adaptéry implementujú rozhrania výstupných portov. PostgreSQL repozitár, SMTP odosielateľ e-mailov, Stripe platobná brána. Všetko toto sú výstupné adaptéry.

Kľúčový poznatok: adaptéry závisia na portoch. Porty nikdy nezávisia na adaptéroch. Toto je to, čo celý vzor robí funkčným.

Konkrétny príklad

Povedzme, že budujete funkciu zadávania objednávok. Tu je, ako sa vrstvy rozkladajú.

Doména (entity a pravidlá):

// domain/Order.ts
class Order {
  readonly id: string;
  readonly items: OrderItem[];
  readonly status: OrderStatus;

  calculateTotal(): Money {
    return this.items.reduce(
      (sum, item) => sum.add(item.price.multiply(item.quantity)),
      Money.zero()
    );
  }

  confirm(): Order {
    if (this.items.length === 0) {
      throw new EmptyOrderError();
    }
    return new Order({ ...this, status: "confirmed" });
  }
}

Vstupný port (rozhranie prípadu použitia):

// ports/inbound/PlaceOrder.ts
interface PlaceOrder {
  execute(command: PlaceOrderCommand): Promise<OrderConfirmation>;
}

Výstupné porty (čo prípad použitia potrebuje):

// ports/outbound/OrderRepository.ts
interface OrderRepository {
  save(order: Order): Promise<void>;
  findById(id: string): Promise<Order | null>;
}

// ports/outbound/PaymentGateway.ts
interface PaymentGateway {
  charge(amount: Money, method: PaymentMethod): Promise<PaymentResult>;
}

// ports/outbound/NotificationSender.ts
interface NotificationSender {
  sendOrderConfirmation(order: Order): Promise<void>;
}

Aplikačná služba (implementuje vstupný port):

// application/PlaceOrderService.ts
class PlaceOrderService implements PlaceOrder {
  constructor(
    private orders: OrderRepository,
    private payments: PaymentGateway,
    private notifications: NotificationSender
  ) {}

  async execute(command: PlaceOrderCommand): Promise<OrderConfirmation> {
    const order = Order.create(command.items);
    const confirmed = order.confirm();

    const payment = await this.payments.charge(
      confirmed.calculateTotal(),
      command.paymentMethod
    );

    if (!payment.successful) {
      throw new PaymentFailedError(payment.reason);
    }

    await this.orders.save(confirmed);
    await this.notifications.sendOrderConfirmation(confirmed);

    return OrderConfirmation.from(confirmed, payment);
  }
}

Adaptéry (infraštruktúra):

// adapters/inbound/OrderController.ts
class OrderController {
  constructor(private placeOrder: PlaceOrder) {}

  async handlePost(req: Request): Promise<Response> {
    const command = PlaceOrderCommand.fromRequest(req.body);
    const confirmation = await this.placeOrder.execute(command);
    return Response.json(confirmation.toResponse(), { status: 201 });
  }
}

// adapters/outbound/PostgresOrderRepository.ts
class PostgresOrderRepository implements OrderRepository {
  async save(order: Order): Promise<void> {
    await this.db.query(
      "INSERT INTO orders (id, items, status) VALUES ($1, $2, $3)",
      [order.id, JSON.stringify(order.items), order.status]
    );
  }

  async findById(id: string): Promise<Order | null> {
    const row = await this.db.query("SELECT * FROM orders WHERE id = $1", [id]);
    return row ? Order.fromPersistence(row) : null;
  }
}

Všimnite si, že PlaceOrderService nikdy nespomína PostgreSQL, HTTP ani žiadny framework. Pracuje čisto s doménovými konceptmi a rozhraniami portov.

Pravidlo závislostí

Toto je najdôležitejšie pravidlo. Ak si z tohto článku odnesiete len jednu vec, nech je to táto:

Závislosti zdrojového kódu musia vždy smerovať dovnútra, k doméne.

  • Adaptéry závisia na portoch. Nikdy naopak.
  • Porty závisia na doménových entitách. Nikdy na adaptéroch.
  • Doména nezávisí na ničom externom.

Toto sa vynucuje prostredníctvom dependency injection. Váš composition root (vstupný bod aplikácie) všetko prepojí:

// main.ts (composition root)
const orderRepo = new PostgresOrderRepository(db);
const payments = new StripePaymentGateway(stripeClient);
const notifications = new EmailNotificationSender(mailer);

const placeOrder = new PlaceOrderService(orderRepo, payments, notifications);
const controller = new OrderController(placeOrder);

Doménová vrstva nikdy neimportuje z vrstvy adaptérov. Ak vidíte import z adapters/ vo vnútri domain/ alebo ports/, niečo je zle.

Štruktúra projektu

Čistá adresárová štruktúra robí hranice viditeľnými:

src/
  domain/
    Order.ts
    OrderItem.ts
    Money.ts
    errors/
      EmptyOrderError.ts
      PaymentFailedError.ts
  ports/
    inbound/
      PlaceOrder.ts
      CancelOrder.ts
    outbound/
      OrderRepository.ts
      PaymentGateway.ts
      NotificationSender.ts
  application/
    PlaceOrderService.ts
    CancelOrderService.ts
  adapters/
    inbound/
      http/
        OrderController.ts
      cli/
        PlaceOrderCommand.ts
    outbound/
      persistence/
        PostgresOrderRepository.ts
      payment/
        StripePaymentGateway.ts
      notification/
        EmailNotificationSender.ts
  main.ts

Každý vývojár v tíme sa môže pozrieť na tento strom a pochopiť, kam umiestniť nový kód.

Výhody pre testovanie

Tu sa hexagonálna architektúra skutočne vypláca.

Unit testovanie domény:

test("order calculates total correctly", () => {
  const order = Order.create([
    { product: "Widget", price: Money.eur(10), quantity: 3 },
    { product: "Gadget", price: Money.eur(25), quantity: 1 },
  ]);

  expect(order.calculateTotal()).toEqual(Money.eur(55));
});

Žiadna databáza. Žiadny HTTP server. Žiadny mockovací framework. Čistá logika, čisté testy.

Testovanie prípadov použitia s fake adaptérmi:

test("place order charges payment and saves", async () => {
  const orders = new InMemoryOrderRepository();
  const payments = new FakePaymentGateway({ alwaysSucceeds: true });
  const notifications = new FakeNotificationSender();

  const service = new PlaceOrderService(orders, payments, notifications);

  const result = await service.execute({
    items: [{ product: "Widget", price: 10, quantity: 2 }],
    paymentMethod: { type: "card", token: "tok_test" },
  });

  expect(result.status).toBe("confirmed");
  expect(orders.savedOrders).toHaveLength(1);
  expect(payments.charges).toHaveLength(1);
  expect(notifications.sent).toHaveLength(1);
});

Testujete celý prípad použitia bez dotyku skutočnej infraštruktúry. Fake adaptéry implementujú rovnaké rozhrania portov, takže sú zameniteľné so skutočnými.

Integračné testy len pre adaptéry:

test("PostgresOrderRepository saves and retrieves", async () => {
  const repo = new PostgresOrderRepository(testDb);
  const order = Order.create([
    { product: "Widget", price: Money.eur(10), quantity: 1 },
  ]);

  await repo.save(order.confirm());
  const found = await repo.findById(order.id);

  expect(found).toEqual(order.confirm());
});

Každý adaptér dostane vlastný integračný test. Ak adaptér prejde testom a prípad použitia prejde s fake adaptérmi, celý systém funguje.

Kedy použiť hexagonálnu architektúru

Hexagonálna architektúra pridáva štruktúru a indirection. To má svoju cenu. Tu je, kedy sa to oplatí:

  • Dlhoveké aplikácie. Ak sa projekt bude udržiavať roky, investícia sa rýchlo vráti.
  • Komplexné obchodné pravidlá. Ak vaša doména má netriviálnu logiku, ktorá potrebuje byť dôkladne otestovaná.
  • Viaceré vstupné body. Ak je rovnaká logika volaná z webového API, CLI, fronty správ a cron úlohy.
  • Infraštruktúra sa môže zmeniť. Ak si nie ste istí, či zostanete pri vašej aktuálnej databáze, poskytovateľovi cloudu alebo službách tretích strán.
  • Viaceré vývojári. Jasné hranice znižujú konflikty pri merge a zrýchľujú code review.

Kedy to preskočiť

Nie každý projekt potrebuje túto úroveň štruktúry:

  • Jednoduché CRUD aplikácie. Ak aplikácia väčšinou číta a zapisuje dáta s minimálnou obchodnou logikou, indirection pridáva náklady bez úžitku.
  • Prototypy a MVP. Rýchlosť je dôležitejšia ako architektúra, keď validujete nápad. Refaktorujte neskôr.
  • Malé skripty a nástroje. 200-riadkový CLI nástroj nepotrebuje porty a adaptéry.

Správna miera architektúry závisí od zložitosti problému. Začnite jednoducho. Zaveďte hexagonálne hranice, keď sa bolesť z ich absencie stane reálnou.

Bežné chyby

Únik infraštruktúry do domény. Ak vaša entita Order má dekorátor @Column alebo metódu toJSON(), infraštruktúra prenikla dovnútra. Udržiavajte vaše doménové objekty čisté.

Vytváranie portov, ktoré kopírujú infraštruktúru. Port s názvom SqlQuery marí účel. Porty by mali opisovať, čo doména potrebuje, v doménovom jazyku, nie ako infraštruktúra funguje.

Nadmerná abstrakcia. Nie všetko potrebuje port. Ak máte utility funkciu na formátovanie dátumov, jednoducho ju použite priamo. Porty vyhraďte pre skutočné infraštruktúrne hranice.

Vynechanie composition root. Bez jasného miesta, kde sa závislosti prepojia, vzor sa rozpadne. Každá závislosť by mala byť injektovaná, nie importovaná priamo.

Hexagonálna vs. iné vzory

Možno sa pýtate, ako sa hexagonálna architektúra vzťahuje k iným známym vzorom:

  • Clean Architecture (Robert C. Martin): Veľmi podobná. Clean Architecture pridáva viac vrstiev (entity, prípady použitia, interface adaptéry, frameworky), ale jadrová myšlienka je rovnaká: závislosti smerujú dovnútra.
  • Onion Architecture (Jeffrey Palermo): Tiež veľmi podobná. Onion Architecture používa sústredné kruhy s doménou v strede. Terminológia sa líši, ale pravidlo závislostí je identické.
  • Vrstvená architektúra (N-tier): Tradičný prístup, kde sa vrstvy vertikálne skladajú (prezentácia, obchod, dáta). Kľúčový rozdiel: vo vrstvenej architektúre obchodná vrstva zvyčajne závisí na dátovej vrstve. V hexagonálnej architektúre je táto závislosť invertovaná.

Všetky tri moderné vzory (hexagonálny, clean, onion) zdieľajú rovnaký fundamentálny princíp. Doména nezávisí na infraštruktúre. Líšia sa väčšinou v konvenciách pomenovania a počte vrstiev, ktoré predpisujú.

Ako začať

Ak chcete aplikovať hexagonálnu architektúru na existujúci projekt, nesnažte sa refaktorovať všetko naraz. Začnite s jedným bounded context alebo jednou funkciou:

  1. Identifikujte jadrovú obchodnú logiku v tejto funkcii. Extrahujte ju do čistých funkcií alebo tried bez závislostí na frameworku.
  2. Definujte porty pre externé závislosti, ktoré funkcia používa. Vytvorte rozhrania v doménovom jazyku.
  3. Presuňte existujúci infraštruktúrny kód do tried adaptérov, ktoré implementujú tieto rozhrania.
  4. Prepojte všetko v composition root pomocou dependency injection.
  5. Napíšte testy pre doménu a prípady použitia pomocou fake adaptérov.

Opakujte pre ďalšiu funkciu. Časom sa hexagonálne hranice prirodzene rozšíria naprieč kódovou základňou.

Záverečné myšlienky

Hexagonálna architektúra nie je o dodržiavaní rigidnej šablóny. Je o jednej jednoduchej myšlienke: chráňte svoju obchodnú logiku pred chaosom vonkajšieho sveta. Databázy sa menia. Frameworky vyjdú z módy. API sa deprecatujú. Vaše obchodné pravidlá by mali prežiť toto všetko.

Vzor funguje, pretože je v súlade s tým, ako sa softvér skutočne vyvíja. Požiadavky sa menia neustále. Infraštruktúra sa mení periodicky. Ale jadrové pravidlá vášho podnikania bývajú najstabilnejšou časťou systému. Stavajte okolo tejto stability.

Ak budujete produkt, ktorý musí vydržať, hexagonálna architektúra je jednou z najlepších investícií, ktoré môžete urobiť do svojej kódovej základne.


Potrebujete pomoc s návrhom alebo refaktorovaním architektúry vašej aplikácie? Ozvite sa nám. Pomáhame tímom budovať softvér, ktorý zostáva udržateľný, ako rastie.

architecturehexagonalports and adaptersclean codesoftware design

Poďme postaviť váš ďalší projekt.

Rezervujte si bezplatný 30-minútový hovor. Preberieme vaše ciele, časový plán a najlepší postup. Bez záväzkov.

Rezervovať úvodný hovor hello@ryveris.com