← Blogi
tutorials

Heksagonaalne arhitektuur | Praktiline juhend kaasaegsetele rakendustele

Õppige, mis on heksagonaalne arhitektuur, miks see oluline on ja kuidas seda rakendada. Täielik juhend reaalsete näidetega hooldatava, testitava tarkvara ehitamiseks.

Ryveris Team ·
Heksagonaalne arhitektuur | Praktiline juhend kaasaegsetele rakendustele

Heksagonaalne arhitektuur on tarkvaradisaini muster, mis isoleerib teie põhilise äriloogika välistest süsteemidest nagu andmebaasid, API-d ja kasutajaliidesed. Idee on lihtne: teie domeeni kood ei tohiks kunagi sõltuda infrastruktuurist. Infrastruktuur sõltub domeenist.

Mustri tutvustas Alistair Cockburn 2005. aastal. Võite kuulda seda nimetatavat ka “Pordid ja adapterid.” Mõlemad nimed kirjeldavad sama kontseptsiooni.

Miks see eksisteerib

Enamik rakendusi algab ühtemoodi. Äriloogika saab sassi andmebaasipäringute, HTTP käsitlejate ja kolmandate osapoolte teenuste kutsete sisse. See toimib algul. Siis koodibaas kasvab ja juhtub kolm asja:

  1. Testimine muutub valusaks. Te ei saa ärireegleid testida ilma andmebaasi käivitamata või poole raamistiku mocki’mata.
  2. Infrastruktuuri muutmine on riskantne. Makseteenuse pakkuja vahetamine või REST-ilt GraphQL-ile migreerumine tähendab äriloogika ümberkirjutamist.
  3. Koodi on raske mõista. Keegi ei suuda öelda, kus domeen lõpeb ja infrastruktuur algab.

Heksagonaalne arhitektuur lahendab kõik kolm probleemi, jõustades ühe reegli: sõltuvused osutavad alati sissepoole.

Põhimõisted

Mõelge oma rakendusest kui kolmest kontsentrilisest kihist.

Domeen (keskus)

See on teie äriloogika. Puhtad funktsioonid, olemid, väärtusobjektid ja domeeniteenused. Sellel on null sõltuvust raamistikest, andmebaasidest või välistest teekidest. See räägib oma keelt.

E-kaubanduse süsteemi puhul sisaldab see kiht mõisteid nagu Order, Product, PricingRule ja InventoryPolicy. Need objektid ei tea midagi SQL-ist, HTTP-st ega JSON-ist.

Pordid (keskmine kiht)

Pordid on liidesed, mis määratlevad, kuidas välismaailm domeeniga suhtleb. Need on lepingud, mitte teostused.

On kaks tüüpi:

  • Sissetulevad pordid (juhtivad). Määratlevad, mida rakendus teha saab. Mõelge neist kui kasutusjuhtumitest. “Esitage tellimus.” “Tühistage tellimus.” “Genereerige arve.”
  • Väljaminevad pordid (juhitavad). Määratlevad, mida rakendus välismaailmalt vajab. “Salvestage tellimus.” “Saatke teade.” “Tooge vahetuskurss.”

Pordid on osa domeenikiist. Need on kirjutatud domeenikeeles, mitte infrastruktuurikeeles. Te kirjutate OrderRepository, mitte PostgresOrderDAO.

Adapterid (välimine kiht)

Adapterid on konkreetsed teostused, mis ühendavad reaalmaailma teie portidega.

  • Sissetulevad adapterid tõlgivad välised päringud domeeni kutseteks. REST kontroller, GraphQL resolver, CLI käsk, sõnumijärjekorra tarbija. Need kõik on sissetulevad adapterid.
  • Väljaminevad adapterid teostavad väljamineva pordi liideseid. PostgreSQL hoidla, SMTP e-posti saatja, Stripe makseteenuse lüüs. Need kõik on väljaminevad adapterid.

Peamine taipamine: adapterid sõltuvad portidest. Pordid ei sõltu kunagi adapteritest. See on see, mis paneb kogu mustri tööle.

Konkreetne näide

Oletame, et ehitate tellimuse esitamise funktsiooni. Siin on, kuidas kihid jaotuvad.

Domeen (olemid ja reeglid):

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

Sissetulev port (kasutusjuhumi liides):

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

Väljaminevad pordid (mida kasutusjuht vajab):

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

Rakendusteenus (teostab sissetuleva pordi):

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

Adapterid (infrastruktuur):

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

Pange tähele, kuidas PlaceOrderService ei maini kunagi PostgreSQL-i, HTTP-d ega ühtegi raamistikku. See töötab puhtalt domeeni mõistete ja pordi liidestega.

Sõltuvusreegel

See on kõige olulisem reegel. Kui te sellest artiklist midagi muud ei saa, saage see:

Lähtekoodi sõltuvused peavad alati osutama sissepoole, domeeni suunas.

  • Adapterid sõltuvad portidest. Mitte kunagi vastupidi.
  • Pordid sõltuvad domeeni olemitest. Mitte kunagi adapteritest.
  • Domeen ei sõltu millestki välisest.

Seda jõustatakse sõltuvuse süstimise kaudu. Teie kompositsiooni juur (rakenduse sisenemispunkt) ühendab kõik kokku:

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

Domeenikiht ei impordi kunagi adapterite kihist. Kui näete importi adapters/ kaustast domain/ või ports/ sees, on midagi valesti.

Projekti struktuur

Puhas kaustade struktuur muudab piirid nähtavaks:

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

Iga meeskonna arendaja saab seda puud vaadata ja aru saada, kuhu uut koodi panna.

Testimise eelised

Siin on koht, kus heksagonaalne arhitektuur tõeliselt ennast ära tasub.

Domeeni ühiktestimine:

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

Andmebaasi pole. HTTP serverit pole. Mockimisraamistikku pole. Puhas loogika, puhtad testid.

Kasutusjuhtumite testimine vale-adapteritega:

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

Testite kogu kasutusjuhumi ilma tegeliku infrastruktuuri puudutamata. Vale-adapterid teostavad samu pordi liideseid, nii et need on tegelikega vahetatavad.

Integratsioonitestid ainult adapterite jaoks:

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

Iga adapter saab oma integratsioonitesti. Kui adapter läbib oma testi ja kasutusjuht läbib vale-adapteritega, töötab kogu süsteem.

Millal kasutada heksagonaalset arhitektuuri

Heksagonaalne arhitektuur lisab struktuuri ja kaudset suunamist. Sellel on hind. Siin on, millal see on seda väärt:

  • Pikaajalised rakendused. Kui projekti hooldatakse aastaid, tasub investeering end kiiresti ära.
  • Keerukad ärireeglid. Kui teie domeenis on mittetriviaalne loogika, mida tuleb põhjalikult testida.
  • Mitu sisenemispunkti. Kui sama loogikat kutsutakse veebi API-st, CLI-st, sõnumijärjekorrast ja cron-tööst.
  • Infrastruktuur võib muutuda. Kui te pole kindel, kas jääte oma praeguse andmebaasi, pilveteenuse pakkuja või kolmanda osapoole teenuste juurde.
  • Mitu arendajat. Selged piirid vähendavad liitmiskonflikte ja muudavad koodiülevaatused kiiremaks.

Millal seda vahele jätta

Mitte iga projekt ei vaja seda struktuuri taset:

  • Lihtsad CRUD rakendused. Kui rakendus on peamiselt andmete lugemine ja kirjutamine minimaalse äriloogikaga, lisab kaudne suunamine kulu ilma kasuta.
  • Prototüübid ja MVP-d. Kiirus on olulisem kui arhitektuur, kui valideerite ideed. Refaktorige hiljem.
  • Väikesed skriptid ja tööriistad. 200-realine CLI tööriist ei vaja porte ja adaptereid.

Arhitektuuri õige hulk sõltub probleemi keerukusest. Alustage lihtsalt. Lisage heksagonaalsed piirid, kui nende puudumise valu muutub reaalseks.

Levinud vead

Infrastruktuuri lekkimine domeeni. Kui teie Order olemil on @Column dekoraator või toJSON() meetod, on infrastruktuur sisse lekkinud. Hoidke oma domeeni objektid puhtana.

Portide loomine, mis peegeldavad infrastruktuuri. Port nimega SqlQuery nurjab eesmärki. Pordid peaksid kirjeldama, mida domeen vajab domeenikeeles, mitte seda, kuidas infrastruktuur töötab.

Üleabstraktsioon. Kõik ei vaja porti. Kui teil on utilitaarfunktsioon, mis formaadib kuupäevi, kasutage seda lihtsalt otse. Reserveerige pordid tegelike infrastruktuuripiiride jaoks.

Kompositsiooni juure vahele jätmine. Ilma selge kohata, kus sõltuvused kokku ühendatakse, laguneb muster. Iga sõltuvus peaks olema süstitud, mitte otse imporditud.

Heksagonaalne vs teised mustrid

Võite mõelda, kuidas heksagonaalne arhitektuur suhtub teiste tuntud mustritega:

  • Puhas arhitektuur (Robert C. Martin): Väga sarnane. Puhas arhitektuur lisab rohkem kihte (olemid, kasutusjuhud, liidese adapterid, raamistikud), kuid põhiidee on sama: sõltuvused osutavad sissepoole.
  • Sibula arhitektuur (Jeffrey Palermo): Samuti väga sarnane. Sibula arhitektuur kasutab kontsentrilisi ringe, mille keskel on domeen. Terminoloogia erineb, kuid sõltuvusreegel on identne.
  • Kihiline arhitektuur (N-tier): Traditsiooniline lähenemine, kus kihid ladestuvad vertikaalselt (esitlus, äri, andmed). Kriitiline erinevus: kihilises arhitektuuris sõltub ärikihtt tavaliselt andmekihist. Heksagonaalses arhitektuuris on see sõltuvus pööratud.

Kõik kolm kaasaegset mustrit (heksagonaalne, puhas, sibul) jagavad sama fundamentaalset põhimõtet. Domeen ei sõltu infrastruktuurist. Need erinevad peamiselt nimetamise konventsioonides ja ettekirjutatud kihtide arvus.

Alustamine

Kui soovite rakendada heksagonaalset arhitektuuri olemasolevale projektile, ärge proovige kõike korraga refaktorida. Alustage ühe piiratud konteksti või ühe funktsiooniga:

  1. Tuvastage selle funktsiooni põhiline äriloogika. Eraldage see puhtate funktsioonide või klasside sisse ilma raamistiku sõltuvusteta.
  2. Määratlege pordid välistele sõltuvustele, mida see funktsioon kasutab. Looge liidesed domeenikeeles.
  3. Viige olemasolev infrastruktuurikood adapteriklassidesse, mis teostavad neid liideseid.
  4. Ühendage kõik kokku kompositsiooni juures sõltuvuse süstimise abil.
  5. Kirjutage testid domeeni ja kasutusjuhtumite jaoks, kasutades vale-adaptereid.

Korrake järgmise funktsiooni jaoks. Aja jooksul levivad heksagonaalsed piirid loomulikult üle kogu koodibaasi.

Lõppmõtted

Heksagonaalne arhitektuur ei ole jäiga malli järgimine. See on üks lihtne idee: kaitske oma äriloogikat välismaailma kaose eest. Andmebaasid muutuvad. Raamistikud lähevad moest. API-d aeguvad. Teie ärireeglid peaksid kõigele sellele vastu pidama.

Muster töötab, sest see ühtib sellega, kuidas tarkvara tegelikult areneb. Nõuded muutuvad pidevalt. Infrastruktuur muutub perioodiliselt. Kuid teie ettevõtte põhireeglid kipuvad olema süsteemi kõige stabiilsem osa. Ehitage selle stabiilsuse ümber.

Kui ehitate toodet, mis peab kestma, on heksagonaalne arhitektuur üks parimaid investeeringuid, mida saate oma koodibaasi teha.


Vajate abi oma rakenduse arhitektuuri kujundamisel või refaktorimisel? Võtke ühendust. Aitame meeskondadel ehitada tarkvara, mis jääb hooldatavaks kasvu käigus.

architecturehexagonalports and adaptersclean codesoftware design

Ehitame koos teie järgmise projekti.

Broneeri tasuta 30-minutiline kõne. Arutame teie eesmärke, ajakava ja parimat lähenemist. Kohustusevaba.

Broneeri tutvumiskõne hello@ryveris.com