← Blog
tutorials

Hexagonální architektura | Praktický průvodce pro moderní aplikace

Zjistěte, co je hexagonální architektura, proč záleží a jak ji implementovat. Kompletní průvodce se skutečnými příklady pro budování udržovatelného, testovatelného softwaru.

Ryveris Team ·
Hexagonální architektura | Praktický průvodce pro moderní aplikace

Hexagonální architektura je návrhový vzor softwaru, který izoluje vaši hlavní obchodní logiku od externích systémů jako databáze, API a uživatelská rozhraní. Myšlenka je jednoduchá: váš doménový kód by nikdy neměl záviset na infrastruktuře. Infrastruktura závisí na doméně.

Vzor představil Alistair Cockburn v roce 2005. Můžete se s ním setkat také pod názvem “Ports and Adapters.” Oba názvy popisují stejný koncept.

Proč existuje

Většina aplikací začíná stejně. Obchodní logika se zamotá s databázovými dotazy, HTTP handlery a voláními služeb třetích stran. Zpočátku to funguje. Pak kódová základna roste a dějí se tři věci:

  1. Testování se stává bolestivým. Nemůžete testovat obchodní pravidla bez spuštění databáze nebo mockování poloviny frameworku.
  2. Změna infrastruktury je riziková. Výměna poskytovatele plateb nebo migrace z REST na GraphQL znamená přepisování obchodní logiky.
  3. Kód je těžké pochopit. Nikdo nerozezná, kde končí doména a začíná infrastruktura.

Hexagonální architektura řeší všechny tři problémy vynucením jediného pravidla: závislosti vždy směřují dovnitř.

Základní koncepty

Představte si svou aplikaci jako tři soustředné vrstvy.

Doména (střed)

Toto je vaše obchodní logika. Čisté funkce, entity, hodnotové objekty a doménové služby. Má nulové závislosti na frameworcích, databázích nebo externích knihovnách. Mluví svým vlastním jazykem.

Pro e-commerce systém tato vrstva obsahuje koncepty jako Order, Product, PricingRule a InventoryPolicy. Tyto objekty nevědí nic o SQL, HTTP nebo JSON.

Porty (střední vrstva)

Porty jsou rozhraní, která definují, jak vnější svět interaguje s doménou. Jsou to kontrakty, ne implementace.

Existují dva typy:

  • Vstupní porty (driving). Definují, co aplikace umí dělat. Představte si je jako případy použití. “Zadat objednávku.” “Zrušit předplatné.” “Vygenerovat fakturu.”
  • Výstupní porty (driven). Definují, co aplikace potřebuje od vnějšího světa. “Uložit objednávku.” “Odeslat notifikaci.” “Načíst směnný kurz.”

Porty jsou součástí doménové vrstvy. Jsou psány v doménovém jazyce, ne v jazyce infrastruktury. Píšete OrderRepository, ne PostgresOrderDAO.

Adaptéry (vnější vrstva)

Adaptéry jsou konkrétní implementace, které propojují reálný svět s vašimi porty.

  • Vstupní adaptéry překládají externí požadavky na doménová volání. REST kontroler, GraphQL resolver, CLI příkaz, konzument fronty zpráv. Všechny tyto jsou vstupní adaptéry.
  • Výstupní adaptéry implementují rozhraní výstupních portů. PostgreSQL repozitář, SMTP odesílač e-mailů, Stripe platební brána. Všechny tyto jsou výstupní adaptéry.

Klíčový poznatek: adaptéry závisí na portech. Porty nikdy nezávisí na adaptérech. Toto je to, co celý vzor pohání.

Konkrétní příklad

Řekněme, že budujete funkci zadání objednávky. Zde je rozložení vrstev.

Doména (entity a pravidla):

// 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 (rozhraní případu použití):

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

Výstupní porty (co případ použití potřebuje):

// 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 (infrastruktura):

// 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šimněte si, že PlaceOrderService nikdy nezmiňuje PostgreSQL, HTTP ani žádný framework. Pracuje čistě s doménovými koncepty a rozhraními portů.

Pravidlo závislostí

Toto je nejdůležitější pravidlo. Pokud si z tohoto článku odnesete jen jednu věc, ať je to tato:

Závislosti zdrojového kódu musí vždy směřovat dovnitř, k doméně.

  • Adaptéry závisí na portech. Nikdy naopak.
  • Porty závisí na doménových entitách. Nikdy na adaptérech.
  • Doména nezávisí na ničem externím.

Toto se vynucuje prostřednictvím dependency injection. Váš composition root (vstupní bod aplikace) vše propojí:

// 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érů. Pokud vidíte import z adapters/ uvnitř domain/ nebo ports/, něco je špatně.

Struktura projektu

Čistá adresářová struktura dělá hranice viditelný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ář v týmu se může podívat na tento strom a pochopit, kam umístit nový kód.

Výhody pro testování

Zde se hexagonální architektura skutečně vyplatí.

Unit testování 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));
});

Žádná databáze. Žádný HTTP server. Žádný mockovací framework. Čistá logika, čisté testy.

Testování případů použití s fake adaptéry:

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ý případ použití bez dotýkání reálné infrastruktury. Fake adaptéry implementují stejná portová rozhraní, takže jsou zaměnitelné se skutečnými.

Integrační testy jen pro 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 svůj vlastní integrační test. Pokud adaptér projde svým testem a případ použití projde s fake adaptéry, celý systém funguje.

Kdy používat hexagonální architekturu

Hexagonální architektura přidává strukturu a indirection. To má svou cenu. Zde je, kdy se to vyplatí:

  • Dlouhodobé aplikace. Pokud bude projekt udržován roky, investice se rychle vrátí.
  • Složitá obchodní pravidla. Pokud má vaše doména netriviální logiku, která potřebuje být důkladně testována.
  • Více vstupních bodů. Pokud je stejná logika volána z webového API, CLI, fronty zpráv a cron jobu.
  • Infrastruktura se může měnit. Pokud si nejste jisti, zda zůstanete u aktuální databáze, cloudového poskytovatele nebo služeb třetích stran.
  • Více vývojářů. Jasné hranice snižují konflikty při merge a zrychlují code review.

Kdy to přeskočit

Ne každý projekt potřebuje tuto úroveň struktury:

  • Jednoduché CRUD aplikace. Pokud aplikace převážně čte a zapisuje data s minimální obchodní logikou, indirection přidává náklady bez přínosu.
  • Prototypy a MVP. Při validaci nápadu záleží na rychlosti více než na architektuře. Refaktorujte později.
  • Malé skripty a nástroje. CLI nástroj o 200 řádcích nepotřebuje ports and adapters.

Správné množství architektury závisí na složitosti problému. Začněte jednoduše. Zaveďte hexagonální hranice, když se bolest z jejich absence stane reálnou.

Časté chyby

Prosakování infrastruktury do domény. Pokud vaše entita Order má dekorátor @Column nebo metodu toJSON(), infrastruktura prosakuje dovnitř. Udržujte doménové objekty čisté.

Vytváření portů, které zrcadlí infrastrukturu. Port s názvem SqlQuery maří účel. Porty by měly popisovat, co doména potřebuje v doménovém jazyce, ne jak funguje infrastruktura.

Přílišná abstrakce. Ne vše potřebuje port. Pokud máte utility funkci, která formátuje data, jednoduše ji použijte přímo. Rezervujte porty pro skutečné infrastrukturní hranice.

Vynechání composition root. Bez jasného místa, kde jsou závislosti propojeny, vzor se rozpadá. Každá závislost by měla být injektována, ne přímo importována.

Hexagonální vs. jiné vzory

Možná se ptáte, jak hexagonální architektura souvisí s jinými známými vzory:

  • Clean Architecture (Robert C. Martin): Velmi podobná. Clean Architecture přidává více vrstev (entity, případy použití, interface adaptéry, frameworky), ale jádro myšlenky je stejné: závislosti směřují dovnitř.
  • Onion Architecture (Jeffrey Palermo): Také velmi podobná. Onion Architecture používá soustředné kruhy s doménou ve středu. Terminologie se liší, ale pravidlo závislostí je identické.
  • Vrstvená architektura (N-tier): Tradiční přístup, kde se vrstvy skládají vertikálně (prezentace, business, data). Zásadní rozdíl: ve vrstvené architektuře obchodní vrstva typicky závisí na datové vrstvě. V hexagonální architektuře je tato závislost invertována.

Všechny tři moderní vzory (hexagonální, clean, onion) sdílejí stejný základní princip. Doména nezávisí na infrastruktuře. Liší se převážně v konvencích pojmenování a počtu předepsaných vrstev.

Jak začít

Pokud chcete aplikovat hexagonální architekturu na existující projekt, nepokoušejte se refaktorovat vše najednou. Začněte s jedním bounded contextem nebo jednou funkcí:

  1. Identifikujte hlavní obchodní logiku v dané funkci. Extrahujte ji do čistých funkcí nebo tříd bez závislostí na frameworku.
  2. Definujte porty pro externí závislosti, které daná funkce používá. Vytvořte rozhraní v doménovém jazyce.
  3. Přesuňte existující infrastrukturní kód do tříd adaptérů, které implementují tato rozhraní.
  4. Propojte vše v composition root pomocí dependency injection.
  5. Napište testy pro doménu a případy použití s využitím fake adaptérů.

Opakujte pro další funkci. Postupem času se hexagonální hranice přirozeně rozšíří po celé kódové základně.

Závěrečné myšlenky

Hexagonální architektura není o dodržování rigidní šablony. Je o jedné jednoduché myšlence: chraňte svou obchodní logiku před chaosem vnějšího světa. Databáze se mění. Frameworky vycházejí z módy. API jsou deprecated. Vaše obchodní pravidla by měla přežít to vše.

Vzor funguje, protože odpovídá tomu, jak se software skutečně vyvíjí. Požadavky se neustále mění. Infrastruktura se mění periodicky. Ale základní pravidla vašeho podnikání bývají nejstabilnější částí systému. Budujte kolem této stability.

Pokud budujete produkt, který má vydržet, hexagonální architektura je jedna z nejlepších investic, kterou můžete do své kódové základny udělat.


Potřebujete pomoc s návrhem nebo refaktoringem architektury vaší aplikace? Ozvěte se nám. Pomáháme týmům budovat software, který zůstane udržovatelný, jak roste.

architecturehexagonalports and adaptersclean codesoftware design

Pojďme vytvořit váš další projekt.

Rezervujte si bezplatný 30minutový hovor. Probereme vaše cíle, termíny a nejlepší přístup. Bez závazku.

Rezervovat konzultaci hello@ryveris.com