← Tinklaraštis
tutorials

Šešiakampė architektūra | Praktinis vadovas šiuolaikinėms programoms

Sužinokite, kas yra šešiakampė architektūra, kodėl ji svarbi ir kaip ją įgyvendinti. Pilnas vadovas su tikrais pavyzdžiais prižiūrimai, testuojamai programinei įrangai kurti.

Ryveris Team ·
Šešiakampė architektūra | Praktinis vadovas šiuolaikinėms programoms

Šešiakampė architektūra yra programinės įrangos dizaino modelis, kuris izoliuoja jūsų pagrindinę verslo logiką nuo išorinių sistemų kaip duomenų bazės, API ir vartotojo sąsajos. Idėja paprasta: jūsų domeno kodas niekada neturėtų priklausyti nuo infrastruktūros. Infrastruktūra priklauso nuo domeno.

Modelį 2005 m. pristatė Alistair Cockburn. Galite jį girdėti vadinamą „Ports and Adapters”. Abu pavadinimai aprašo tą pačią koncepciją.

Kodėl ji egzistuoja

Dauguma programų prasideda vienodai. Verslo logika susipina su duomenų bazės užklausomis, HTTP tvarkyklėmis ir trečiųjų šalių paslaugų iškvietimais. Iš pradžių tai veikia. Tada kodas auga, ir nutinka trys dalykai:

  1. Testavimas tampa skausmingas. Negalite testuoti verslo taisyklių nepaleisdami duomenų bazės ar nemockindami pusės karkaso.
  2. Infrastruktūros keitimas rizikingas. Mokėjimų tiekėjo keitimas ar migracija iš REST į GraphQL reiškia verslo logikos perrašymą.
  3. Kodą sunku suprasti. Niekas negali pasakyti, kur baigiasi domenas ir prasideda infrastruktūra.

Šešiakampė architektūra sprendžia visas tris problemas įvesdama vieną taisyklę: priklausomybės visada rodo į vidų.

Pagrindinės koncepcijos

Galvokite apie savo programą kaip apie tris koncentrinius sluoksnius.

Domenas (centras)

Tai jūsų verslo logika. Grynos funkcijos, esybės, vertės objektai ir domeno paslaugos. Jis neturi jokių priklausomybių nuo karkasų, duomenų bazių ar išorinių bibliotekų. Jis kalba savo kalba.

E-komercijos sistemai šis sluoksnis apima koncepcijas kaip Order, Product, PricingRule ir InventoryPolicy. Šie objektai nieko nežino apie SQL, HTTP ar JSON.

Portai (vidurinis sluoksnis)

Portai yra sąsajos, apibrėžiančios, kaip išorinis pasaulis sąveikauja su domenu. Tai sutartys, ne įgyvendinimai.

Yra du tipai:

  • Įeinantys portai (varančioji pusė). Apibrėžia, ką programa gali daryti. Galvokite apie juos kaip naudojimo atvejus. „Pateikti užsakymą.” „Atšaukti prenumeratą.” „Sugeneruoti sąskaitą.”
  • Išeinantys portai (varomoji pusė). Apibrėžia, ko programai reikia iš išorinio pasaulio. „Išsaugoti užsakymą.” „Siųsti pranešimą.” „Gauti valiutos kursą.”

Portai yra domeno sluoksnio dalis. Jie rašomi domeno kalba, ne infrastruktūros kalba. Rašote OrderRepository, ne PostgresOrderDAO.

Adapteriai (išorinis sluoksnis)

Adapteriai yra konkretūs įgyvendinimai, jungiantys tikrą pasaulį prie jūsų portų.

  • Įeinantys adapteriai verčia išorines užklausas į domeno iškvietimus. REST valdiklis, GraphQL resolveris, CLI komanda, žinučių eilės vartotojas. Visa tai yra įeinantys adapteriai.
  • Išeinantys adapteriai įgyvendina išeinančių portų sąsajas. PostgreSQL saugykla, SMTP el. pašto siuntėjas, Stripe mokėjimų šliuzas. Visa tai yra išeinantys adapteriai.

Pagrindinė įžvalga: adapteriai priklauso nuo portų. Portai niekada nepriklauso nuo adapterių. Tai yra tai, kas visą modelį daro veikiantį.

Konkretus pavyzdys

Tarkime, kuriate užsakymo pateikimo funkciją. Štai kaip sluoksniai pasiskirsto.

Domenas (esybės ir taisyklės):

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

Įeinantis portas (naudojimo atvejo sąsaja):

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

Išeinantys portai (ko reikia naudojimo atvejui):

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

Programos paslauga (įgyvendina įeinantį 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);
  }
}

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

Atkreipkite dėmesį, kaip PlaceOrderService niekada nemini PostgreSQL, HTTP ar jokio karkaso. Jis dirba tik su domeno koncepcijomis ir portų sąsajomis.

Priklausomybių taisyklė

Tai svarbiausia taisyklė. Jei nieko kito neįsiminisite iš šio straipsnio, įsiminkite tai:

Pirminio kodo priklausomybės visada turi rodyti į vidų, domeno link.

  • Adapteriai priklauso nuo portų. Niekada atvirkščiai.
  • Portai priklauso nuo domeno esybių. Niekada nuo adapterių.
  • Domenas nepriklauso nuo nieko išorinio.

Tai užtikrinama per priklausomybių injekciją. Jūsų kompozicijos šaknis (programos įėjimo taškas) sujungia viską:

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

Domeno sluoksnis niekada neimportuoja iš adapterių sluoksnio. Jei matote importą iš adapters/ viduje domain/ ar ports/, kažkas negerai.

Projekto struktūra

Švari katalogų struktūra padaro ribas matomas:

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

Kiekvienas komandos kūrėjas gali pažiūrėti į šį medį ir suprasti, kur dėti naują kodą.

Testavimo privalumai

Čia šešiakampė architektūra tikrai atsiperka.

Domeno vienetų testavimas:

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

Jokios duomenų bazės. Jokio HTTP serverio. Jokio mockavimo karkaso. Gryna logika, gryni testai.

Naudojimo atvejų testavimas su netikrais adapteriais:

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

Testuojate visą naudojimo atvejį neliesdami tikros infrastruktūros. Netikri adapteriai įgyvendina tas pačias portų sąsajas, todėl jie keičiami su tikraisiais.

Integracijos testai tik adapteriams:

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

Kiekvienas adapteris gauna savo integracijos testą. Jei adapteris praeina testą, o naudojimo atvejis praeina su netikrais adapteriais, visa sistema veikia.

Kada naudoti šešiakampę architektūrą

Šešiakampė architektūra prideda struktūros ir netiesioginio ryšio. Tai turi kainą. Štai kada tai verta:

  • Ilgalaikės programos. Jei projektas bus prižiūrimas metų metus, investicija greitai atsiperka.
  • Sudėtingos verslo taisyklės. Jei jūsų domenas turi netrivialią logiką, kurią reikia kruopščiai testuoti.
  • Keli įėjimo taškai. Jei ta pati logika kviečiama iš žiniatinklio API, CLI, žinučių eilės ir cron užduoties.
  • Infrastruktūra gali keistis. Jei nesate tikri, ar liksite su dabartine duomenų baze, debesų tiekėju ar trečiųjų šalių paslaugomis.
  • Keli kūrėjai. Aiškios ribos sumažina suliejimo konfliktus ir pagreitina kodo peržiūras.

Kada praleisti

Ne kiekvienam projektui reikia šio lygio struktūros:

  • Paprastos CRUD programos. Jei programa daugiausia skaito ir rašo duomenis su minimalia verslo logika, netiesioginis ryšys prideda kaštų be naudos.
  • Prototipai ir MVP. Greitis svarbiau nei architektūra, kai tikrinate idėją. Refaktorinkite vėliau.
  • Maži scenarijai ir įrankiai. 200 eilučių CLI įrankiui nereikia portų ir adapterių.

Tinkamas architektūros kiekis priklauso nuo problemos sudėtingumo. Pradėkite paprastai. Įveskite šešiakampes ribas, kai skausmas be jų tampa realus.

Dažnos klaidos

Infrastruktūros nutekėjimas į domeną. Jei jūsų Order esybė turi @Column dekoratorių ar toJSON() metodą, infrastruktūra nutekėjo. Laikykite savo domeno objektus švariais.

Portų kūrimas, atspindintis infrastruktūrą. Portas pavadinimu SqlQuery nugalina tikslą. Portai turėtų aprašyti, ko domenui reikia domeno kalba, o ne kaip veikia infrastruktūra.

Per didelė abstrakcija. Ne viskam reikia porto. Jei turite pagalbinę funkciją, kuri formatuoja datas, tiesiog naudokite ją. Portus rezervuokite tikroms infrastruktūros riboms.

Kompozicijos šaknies praleidimas. Be aiškios vietos, kur priklausomybės sujungiamos, modelis sugriūva. Kiekviena priklausomybė turėtų būti injektuojama, ne importuojama tiesiogiai.

Šešiakampė vs. kiti modeliai

Galbūt stebitės, kaip šešiakampė architektūra susijusi su kitais gerai žinomais modeliais:

  • Clean Architecture (Robert C. Martin): Labai panašu. Clean Architecture prideda daugiau sluoksnių (esybės, naudojimo atvejai, sąsajų adapteriai, karkasai), bet pagrindinė idėja ta pati: priklausomybės rodo į vidų.
  • Onion Architecture (Jeffrey Palermo): Taip pat labai panašu. Onion Architecture naudoja koncentrinius žiedus su domenu centre. Terminologija skiriasi, bet priklausomybių taisyklė identiška.
  • Sluoksninė architektūra (N-tier): Tradicinis požiūris, kur sluoksniai dedami vertikaliai (prezentacija, verslas, duomenys). Esminis skirtumas: sluoksninėje architektūroje verslo sluoksnis paprastai priklauso nuo duomenų sluoksnio. Šešiakampėje architektūroje ta priklausomybė yra apversta.

Visi trys šiuolaikiniai modeliai (šešiakampis, švarus, svogūno) dalijasi tuo pačiu fundamentaliu principu. Domenas nepriklauso nuo infrastruktūros. Jie skiriasi daugiausia pavadinimų konvencijomis ir nurodytų sluoksnių skaičiumi.

Pradžia

Jei norite taikyti šešiakampę architektūrą esamam projektui, nebandykite refaktorinti visko iš karto. Pradėkite nuo vieno riboto konteksto arba vienos funkcijos:

  1. Nustatykite pagrindinę verslo logiką toje funkcijoje. Ištraukite ją į grynąsias funkcijas ar klases be karkaso priklausomybių.
  2. Apibrėžkite portus išorinėms priklausomybėms, kurias ta funkcija naudoja. Sukurkite sąsajas domeno kalba.
  3. Perkelkite esamą infrastruktūros kodą į adapterių klases, įgyvendinančias tas sąsajas.
  4. Sujunkite viską kompozicijos šaknyje naudodami priklausomybių injekciją.
  5. Parašykite testus domenui ir naudojimo atvejams naudodami netikrus adapterius.

Pakartokite kitai funkcijai. Laikui bėgant šešiakampės ribos natūraliai pasklis per kodų bazę.

Galutinės mintys

Šešiakampė architektūra nėra apie griežto šablono sekimą. Tai apie vieną paprastą idėją: apsaugokite savo verslo logiką nuo išorinio pasaulio chaoso. Duomenų bazės keičiasi. Karkasai išeina iš mados. API tampa pasenusios. Jūsų verslo taisyklės turėtų visa tai išgyventi.

Modelis veikia, nes atitinka tai, kaip programinė įranga iš tikrųjų vystosi. Reikalavimai nuolat keičiasi. Infrastruktūra keičiasi periodiškai. Bet pagrindinės jūsų verslo taisyklės linkusios būti stabiliausia sistemos dalimi. Kurkite aplink tą stabilumą.

Jei kuriate produktą, kuris turi išsilaikyti, šešiakampė architektūra yra viena geriausių investicijų, kurias galite padaryti savo kodų bazėje.


Reikia pagalbos projektuojant ar refaktorinant jūsų programos architektūrą? Susisiekite. Padedame komandoms kurti programinę įrangą, kuri lieka prižiūrima augant.

architecturehexagonalports and adaptersclean codesoftware design

Sukurkime jūsų kitą projektą.

Užsisakykite nemokamą 30 minučių konsultaciją. Aptarsime jūsų tikslus, terminus ir geriausią požiūrį. Be jokių įsipareigojimų.

Užsisakyti konsultaciją hello@ryveris.com