← Blog
tutorials

Hexagonális architektúra | Gyakorlati útmutató modern alkalmazásokhoz

Ismerje meg, mi a hexagonális architektúra, miért fontos, és hogyan implementálja. Teljes útmutató valós példákkal karbantartható, tesztelhető szoftver felépítéséhez.

Ryveris Team ·
Hexagonális architektúra | Gyakorlati útmutató modern alkalmazásokhoz

A hexagonális architektúra egy szoftvertervezési minta, amely elkülöníti az alapvető üzleti logikáját a külső rendszerektől, mint az adatbázisok, API-k és felhasználói felületek. Az ötlet egyszerű: a domain kód soha nem függhet az infrastruktúrától. Az infrastruktúra függ a domaintől.

A mintát Alistair Cockburn vezette be 2005-ben. Hallhatja „Ports and Adapters” (Portok és Adapterek) néven is. Mindkét név ugyanazt a koncepciót írja le.

Miért létezik

A legtöbb alkalmazás ugyanúgy indul. Az üzleti logika összekeveredik az adatbázis-lekérdezésekkel, HTTP kezelőkkel és harmadik fél szolgáltatások hívásaival. Eleinte működik. Aztán a kódbázis növekszik, és három dolog történik:

  1. A tesztelés fájdalmassá válik. Nem tudja tesztelni az üzleti szabályokat adatbázis felállítása vagy a keretrendszer felének mockolása nélkül.
  2. Az infrastruktúra megváltoztatása kockázatos. Egy fizetési szolgáltató cseréje vagy a REST-ről GraphQL-re való migráció üzleti logika újraírását jelenti.
  3. A kód nehezen érthető. Senki nem tudja megmondani, hol végződik a domain és hol kezdődik az infrastruktúra.

A hexagonális architektúra mindhárom problémát megoldja egyetlen szabály érvényesítésével: a függőségek mindig befelé mutatnak.

Az alapfogalmak

Gondoljon az alkalmazására három koncentrikus rétegként.

A domain (közép)

Ez az üzleti logikája. Tiszta függvények, entitások, értékobjektumok és domain szolgáltatások. Nulla függősége van keretrendszerektől, adatbázisoktól vagy külső könyvtáraktól. A saját nyelvén beszél.

Egy e-kereskedelmi rendszernél ez a réteg olyan fogalmakat tartalmaz, mint Order, Product, PricingRule és InventoryPolicy. Ezek az objektumok semmit nem tudnak az SQL-ről, HTTP-ről vagy JSON-ről.

Portok (középső réteg)

A portok interfészek, amelyek meghatározzák, hogyan kommunikál a külvilág a domainnel. Szerződések, nem implementációk.

Két típusuk van:

  • Bemeneti portok (driving). Meghatározzák, mit tud csinálni az alkalmazás. Gondoljon rájuk felhasználási esetekként. „Rendelés feladása.” „Előfizetés lemondása.” „Számla generálása.”
  • Kimeneti portok (driven). Meghatározzák, mire van szüksége az alkalmazásnak a külvilágtól. „Rendelés mentése.” „Értesítés küldése.” „Árfolyam lekérése.”

A portok a domain réteg részei. Domain nyelven íródnak, nem infrastruktúra nyelven. OrderRepository-t ír, nem PostgresOrderDAO-t.

Adapterek (külső réteg)

Az adapterek a konkrét implementációk, amelyek a valós világot kapcsolják a portokhoz.

  • Bemeneti adapterek külső kéréseket fordítanak domain hívásokra. Egy REST vezérlő, egy GraphQL resolver, egy CLI parancs, egy üzenetsor fogyasztó. Mindezek bemeneti adapterek.
  • Kimeneti adapterek implementálják a kimeneti port interfészeket. Egy PostgreSQL repository, egy SMTP e-mail küldő, egy Stripe fizetési átjáró. Mindezek kimeneti adapterek.

A kulcsfontosságú felismerés: az adapterek függnek a portoktól. A portok soha nem függnek az adapterektől. Ez az, ami az egész mintát működővé teszi.

Egy konkrét példa

Tegyük fel, hogy rendelésfeladási funkciót épít. Így oszlanak el a rétegek.

Domain (entitások és szabályok):

// 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" });
  }
}

Bemeneti port (felhasználási eset interfész):

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

Kimeneti portok (amire a felhasználási esetnek szüksége van):

// 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>;
}

Alkalmazásszolgáltatás (implementálja a bemeneti portot):

// 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);
  }
}

Adapterek (infrastruktú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;
  }
}

Figyelje meg, hogy a PlaceOrderService soha nem említi a PostgreSQL-t, HTTP-t vagy bármely keretrendszert. Kizárólag domain fogalmakkal és port interfészekkel dolgozik.

A függőségi szabály

Ez a legfontosabb szabály. Ha semmi mást nem visz el ebből a cikkből, vigye el ezt:

A forráskód függőségeknek mindig befelé kell mutatniuk, a domain felé.

  • Az adapterek függnek a portoktól. Soha nem fordítva.
  • A portok függnek a domain entitásoktól. Soha nem az adapterektől.
  • A domain semmitől külsőtől nem függ.

Ezt dependency injection-nel érvényesítjük. Az Ön composition root-ja (az alkalmazás belépési pontja) köti össze az egészet:

// 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);

A domain réteg soha nem importál az adapterek rétegből. Ha adapters/-ből importot lát a domain/ vagy ports/ belsejében, valami nincs rendben.

Projektstruktúra

Egy tiszta mappastruktúra láthatóvá teszi a határokat:

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

A csapat minden fejlesztője ránézhet erre a fára és megérti, hová kell tenni az új kódot.

Tesztelési előnyök

Itt válik igazán kifizetődővé a hexagonális architektúra.

A domain unit tesztelése:

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));
});

Nincs adatbázis. Nincs HTTP szerver. Nincs mockoló keretrendszer. Tiszta logika, tiszta tesztek.

Felhasználási esetek tesztelése hamis adapterekkel:

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);
});

A teljes felhasználási esetet teszteli valós infrastruktúra érintése nélkül. A hamis adapterek ugyanazokat a port interfészeket implementálják, így felcserélhetők a valódiakkal.

Integrációs tesztek csak az adapterekhez:

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());
});

Minden adapter megkapja a saját integrációs tesztjét. Ha az adapter megfelel a tesztjén, és a felhasználási eset megfelel hamis adapterekkel, az egész rendszer működik.

Mikor használja a hexagonális architektúrát

A hexagonális architektúra struktúrát és indirekciót ad hozzá. Ennek van költsége. Íme, mikor éri meg:

  • Hosszú életű alkalmazások. Ha a projektet évekig karbantartják, a befektetés gyorsan megtérül.
  • Összetett üzleti szabályok. Ha a domainje nem triviális logikát tartalmaz, amelyet alaposan tesztelni kell.
  • Több belépési pont. Ha ugyanazt a logikát hívják web API-ból, CLI-ből, üzenetsorból és cron jobból.
  • Az infrastruktúra változhat. Ha nem biztos, hogy marad a jelenlegi adatbázisánál, felhőszolgáltatójánál vagy harmadik fél szolgáltatásainál.
  • Több fejlesztő. A világos határok csökkentik a merge konfliktusokat és gyorsítják a kód áttekintéseket.

Mikor hagyja ki

Nem minden projektnek van szüksége erre a szintű struktúrára:

  • Egyszerű CRUD alkalmazások. Ha az alkalmazás többnyire adatok olvasásáról és írásáról szól minimális üzleti logikával, az indirekció hozzáadott költséget jelent előny nélkül.
  • Prototípusok és MVP-k. A sebesség fontosabb az architektúránál, amikor egy ötletet validál. Refaktoráljon később.
  • Kis scriptek és eszközök. Egy 200 soros CLI eszköznek nincs szüksége portokra és adapterekre.

Az architektúra megfelelő mennyisége a probléma komplexitásától függ. Kezdje egyszerűen. Vezessen be hexagonális határokat, amikor a hiányuk fájdalma valóssá válik.

Gyakori hibák

Infrastruktúra beszivárgása a domainbe. Ha az Order entitásának van @Column dekorátora vagy toJSON() metódusa, az infrastruktúra beszivárgott. Tartsa tisztán a domain objektumait.

Infrastruktúrát tükröző portok létrehozása. Egy SqlQuery nevű port elvéti a célt. A portoknak le kell írniuk, mire van szüksége a domainnek domain nyelven, nem azt, hogyan működik az infrastruktúra.

Túl-absztrahálás. Nem mindennek kell port. Ha van egy dátumformázó segédfüggvénye, egyszerűen használja közvetlenül. A portokat tartsa fenn a tényleges infrastruktúra határokhoz.

A composition root kihagyása. Egyértelmű hely nélkül, ahol a függőségek össze vannak kötve, a minta szétesik. Minden függőséget injektálni kell, nem közvetlenül importálni.

Hexagonális vs. más minták

Elgondolkodhat, hogyan viszonyul a hexagonális architektúra más ismert mintákhoz:

  • Clean Architecture (Robert C. Martin): Nagyon hasonló. A Clean Architecture több réteget ad hozzá (entitások, felhasználási esetek, interfész adapterek, keretrendszerek), de az alapötlet ugyanaz: a függőségek befelé mutatnak.
  • Onion Architecture (Jeffrey Palermo): Szintén nagyon hasonló. Az Onion Architecture koncentrikus gyűrűket használ a domainnel a középpontban. A terminológia különbözik, de a függőségi szabály azonos.
  • Rétegelt architektúra (N-tier): A hagyományos megközelítés, ahol a rétegek függőlegesen egymásra épülnek (prezentáció, üzleti, adat). A döntő különbség: a rétegelt architektúrában az üzleti réteg jellemzően függ az adat rétegtől. A hexagonális architektúrában ez a függőség megfordul.

Mindhárom modern minta (hexagonális, clean, onion) ugyanazt az alapelvet osztja. A domain nem függ az infrastruktúrától. Főleg az elnevezési konvenciókban és az előírt rétegek számában különböznek.

Kezdő lépések

Ha hexagonális architektúrát akar alkalmazni egy meglévő projektre, ne próbáljon meg mindent egyszerre refaktorálni. Kezdje egy bounded context-tel vagy egy funkcióval:

  1. Azonosítsa az alapvető üzleti logikát abban a funkcióban. Emelje ki tiszta függvényekbe vagy osztályokba keretrendszer-függőségek nélkül.
  2. Határozzon meg portokat a funkció által használt külső függőségekhez. Hozzon létre interfészeket domain nyelven.
  3. Helyezze át a meglévő infrastruktúra kódot adapter osztályokba, amelyek implementálják ezeket az interfészeket.
  4. Kösse össze az egészet egy composition root-ban dependency injection használatával.
  5. Írjon teszteket a domainhez és a felhasználási esetekhez hamis adapterek használatával.

Ismételje meg a következő funkcióval. Idővel a hexagonális határok természetesen elterjednek a kódbázisban.

Záró gondolatok

A hexagonális architektúra nem egy merev sablon követéséről szól. Egyetlen egyszerű ötletről szól: védje az üzleti logikáját a külvilág káoszától. Adatbázisok változnak. Keretrendszerek kimennek a divatból. API-k elavulnak. Az Ön üzleti szabályainak mindezt túl kell élniük.

A minta azért működik, mert összhangban van azzal, ahogyan a szoftver ténylegesen fejlődik. A követelmények állandóan változnak. Az infrastruktúra időszakosan változik. De az üzlete alapvető szabályai általában a rendszer legstabilabb része. Építsen erre a stabilitásra.

Ha olyan terméket épít, amelynek tartania kell, a hexagonális architektúra az egyik legjobb befektetés, amelyet a kódbázisába tehet.


Segítségre van szüksége alkalmazása architektúrájának tervezéséhez vagy refaktorálásához? Lépjen kapcsolatba velünk. Segítünk csapatoknak olyan szoftvert építeni, amely karbantartható marad a növekedéssel.

architecturehexagonalports and adaptersclean codesoftware design

Építsük meg a következő projektedet.

Foglalj egy ingyenes 30 perces hívást. Megbeszéljük a céljaidat, az időkeretet és a legjobb megközelítést. Kötelezettség nélkül.

Foglalj konzultációt hello@ryveris.com