Heksagonala arhitektura | Praktisks celvedis modernam lietojumprogrammam
Uzziniet, kas ir heksagonala arhitektura, kapec ta ir svariga un ka to ieviest. Pilnigs celvedis ar realiem piemeriem uzturamas, testejamas programmaturas buvei.
Heksagonala arhitektura ir programmaturas projektesanas modelis, kas izole jusu biznesa pamatlogiku no arejam sistemam ka datu bazem, API un lietotaja saskarneem. Ideja ir vienkarsa: jusu domena kodam nekad nevajadzetu but atkaarigam no infrastrukturas. Infrastruktura ir atkariga no domena.
Modeli iepazistinaja Alistair Cockburn 2005. gada. Jus to varat dzirdet ari ka “Ports and Adapters.” Abi nosaukumi apraksta vienu un to pasu konceptu.
Kapec tas eksiste
Lielaka dala lietojumprogrammu sakas vienadi. Biznesa logika sapinas ar datu bazes vaicajumiem, HTTP apstradatajiem un tresa puses pakalpojumu izsaukumiem. Sakuma tas stradaa. Tad koda baze aug, un notiek triss lietas:
- Testesana klust sapeiga. Jus nevarat testeeet biznesa noteikumus bez datu bazes palaissanas vai puses ietvara izsmiesanas.
- Infrastrukturas maina ir riskanta. Maksajumu pakalpojumu sniedzeja maina vai migracija no REST uz GraphQL nozime biznesa logikas parrakstisanu.
- Kodu ir gruti saprast. Neviens nevar pateikt, kur beidzas domens un saakas infrastruktura.
Heksagonala arhitektura atrisina visas triss problemas, piiesspieezot vienu noteikumu: atkariibas vienmeer ir verstas uz ieksu.
Pamatkoncepti
Iedomajieties savu lietojumprogrammu ka triss koncentriskus slanus.
Domens (centrs)
Ta ir jusu biznesa logika. Tiras funkcijas, entitijas, vertibu objekti un domena pakalpojumi. Tai ir nulles atkaribu no ietvariem, datu bazem vai arejam biblioteekam. Ta runa sava valoda.
E-komercijas sistemai sis slaans satur konceptus ka Order, Product, PricingRule un InventoryPolicy. Sie objekti neko nezina par SQL, HTTP vai JSON.
Porti (videejais slaans)
Porti ir saskarnes, kas definee, ka areja pasaule mijiedarbojas ar domenu. Tie ir ligumi, nevis ieviesanas.
Ir divi tipi:
- Ienakosie porti (vadosie). Definee, ko lietojumprogramma var dariit. Uztveriet tos ka lietosanas gadiijumus. “Veikt pasutijumu.” “Atcelt abonementu.” “Generet rekinu.”
- Izejosie porti (vadamie). Definee, kas lietojumprogrammai ir vajadzigs no arejas pasaules. “Saglabat pasutijumu.” “Nosutit pazinojumu.” “Iegut valutas kursu.”
Porti ir domena slana dala. Tie ir rakstiti domena valoda, nevis infrastrukturas valoda. Jus rakstat OrderRepository, nevis PostgresOrderDAO.
Adapteri (arejais slaanis)
Adapteri ir konkretas ieviessanas, kas savieno realo pasauli ar jusu portiem.
- Ienakosie adapteri tulko arejus pieprasijumus domena izsaukumos. REST kontrolieris, GraphQL risinatajs, CLI komanda, zinojumu rindas pateretajs. Visi sie ir ienakosie adapteri.
- Izejosie adapteri ievies izejosu portu saskarnes. PostgreSQL repozitorijs, SMTP e-pasta sutiitaajs, Stripe maksajumu varteja. Visi sie ir izejosie adapteri.
Galvenais ieskats: adapteri ir atkarigi no portiem. Porti nekad nav atkarigi no adapteriem. Tas ir tas, kas padara visu modeli darbspeejigu.
Konkrets piemers
Pienemmsim, ka jus buvejat pasutijuma veiksanas funkciju. Luk, ka slani sadalas.
Domens (entitijas un noteikumi):
// 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" });
}
}
Ienakosais ports (lietosanas gadijuma saskarne):
// ports/inbound/PlaceOrder.ts
interface PlaceOrder {
execute(command: PlaceOrderCommand): Promise<OrderConfirmation>;
}
Izejosie porti (kas lietosanas gadijumam vajadzigs):
// 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>;
}
Lietojumprogrammas pakalpojums (ievies ienakoso portu):
// 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);
}
}
Adapteri (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;
}
}
Ieveerojiet, ka PlaceOrderService nekad nemin PostgreSQL, HTTP vai jebkadu ietvaru. Tas stradaa tikai ar domena konceptiem un portu saskarneem.
Atkaribu noteikums
Sis ir svariigakais noteikums. Ja no si raksta neiegstat neko citu, iegustiet so:
Avota koda atkarbam vienmeer jabut verstam uz ieksu, pret domenu.
- Adapteri ir atkarigi no portiem. Nekad otradi.
- Porti ir atkarigi no domena entitijam. Nekad no adapteriem.
- Domens nav atkarigs ne no ka areja.
Tas tiek piespests caur atkaribu injekciju. Jusu kompozicijas sakne (lietojumprogrammas ieejas punkts) sasaista visu kopa:
// main.ts (kompozicijas sakne)
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);
Domena slaanis nekad neimporte no adapteru slana. Ja redzat importu no adapters/ ieksa domain/ vai ports/, kaut kas nav kartiiba.
Projekta struktura
Tira mapu struktura padara robezas redzamas:
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
Katrs izstradatajs komanda var apskatit so koku un saprast, kur likt jaunu kodu.
Testesanas prieksrocibas
Luk, kur heksagonala arhitektura patiesi atmaksajas.
Domena vieniibu testesana:
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));
});
Nav datu bazes. Nav HTTP servera. Nav izsmiesanas ietvara. Tira logika, tiri testi.
Lietosanas gadijumu testesana ar viltootieem adapteriem:
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);
});
Jus testeejat visu lietosanas gadijumu, nepieskaroties realai infrastrukturai. Viltotie adapteri ievies taas pasas portu saskarnes, tapec tie ir aizstajami ar isttajiem.
Integracijas testi tikai adapteriem:
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());
});
Katrs adapteris sanem savu integracijas testu. Ja adapteris iztur savu testu un lietosanas gadijums iztur ar viltotiem adapteriem, visa sistema stradaa.
Kad lietot heksagonalu arhitekturu
Heksagonala arhitektura pievieno strukturu un netiesumu. Tam ir maksa. Luk, kad tas ir ta verts:
- Ilgmuzaigas lietojumprogrammas. Ja projekts tiks uzturets gadiem, investicija atri atmaksaajas.
- Sarezgiti biznesa noteikumi. Ja jusu domenam ir netriviala logika, kas rupigi jatesteee.
- Vairaki ieejas punkti. Ja ta pati logika tiek izsaukta no tiimekla API, CLI, zinojumu rindas un cron uzdevuma.
- Infrastruktura var mainities. Ja neesat paarlieecinati, vai paliklsieet pie savas pasreizejas datu bazes, makonu pakalpojumu snidzeeja vai tresa puses pakalpojumiem.
- Vairaki izstradataji. Skaidras robezas samazina sapluusanas konfliktus un padara koda paarskatus atraakus.
Kad to izlaist
Ne katram projektam ir vajadzigs tads strukturas limenis:
- Vienkaarsas CRUD lietojumprogrammas. Ja lietotne galvenokart lasa un raksta datus ar minimalu biznesa logiku, netiessums pievieno izmaksas bez ieguvuma.
- Prototipi un MVP. Atrums ir svariigaks par arhitekturu, kad validejat ideju. Refaktorejiet velak.
- Mazi skripti un riki. 200 rindu CLI rikam nav vajadzigi porti un adapteri.
Pareizais arhitekturas daudzums ir atkarigs no problemas sarezgiitibas. Saciet vienkarsi. Ieviesiet heksagonalas robezas, kad sapju nav to radusies klust reeala.
Biezas klidas
Infrastrukturas lecka domena. Ja jusu Order entitijai ir @Column dekoratajs vai toJSON() metode, infrastruktura ir ielauzusies. Turiet savus domena objektus tirrus.
Portu veidosana, kas atspoguulo infrastrukturu. Ports ar nosaukumu SqlQuery zaudee jegu. Portiem jabut aprakstam, kas domenam vajadzigs domena valoda, nevis ka infrastruktura stradaa.
Parmeeriga abstraheesana. Ne visam ir vajadzigs ports. Ja jums ir utilitas funkcija, kas formattee datumus, vienkarsi izmantojiet to tiesu. Rezervejiet portus reealam infrastrukturas robezam.
Kompozicijas saknes izlaisana. Bez skaidras vietas, kur atkariibas tiek savienotas, modelis sabruk. Katrai atkariibai jabut injicetai, nevis tiesu importetai.
Heksagonala vs. citi modeli
Jus varat but ieintereseti, ka heksagonala arhitektura attiecas pret citiem pazistamiem modeliem:
- Clean Architecture (Robert C. Martin): Loti lidziga. Clean Architecture pievieno vairak slanu (entitijas, lietosanas gadijumi, saskarnu adapteri, ietvari), bet pamatideja ir ta pati: atkariibas ir verstas uz ieksu.
- Onion Architecture (Jeffrey Palermo): Ari loti lidziiga. Onion Architecture izmanto koncentriskus gredzenus ar domenu centra. Terminologjia atskirras, bet atkaribu noteikums ir identisks.
- Layered Architecture (N-tier): Tradicionala pieeja, kur slani krajas vertikali (prezentacija, bizness, dati). Butiska atskirba: slanainaja arhitektura biznesa slaanis parasti ir atkarigs no datu slana. Heksagonalaja arhitektura si atkarriba ir apgriezta.
Visi triss moderni modeli (heksagonalais, clean, onion) dala vienu un to pasu fundamentalo principu. Domens nav atkarigs no infrastrukturas. Tie atskirras galvenokart nossaukumu konvencijas un preskribeto slanu skaita zina.
Darba saksana
Ja velaties pielietot heksagonalu arhitekturu esosam projektam, nemeegiiniet refaktoreet visu uzreiz. Saciet ar vienu ierobezotu kontekstu vai vienu funkciju:
- Identificejiet biznesa pamatlogiku saja funkcija. Izvelciet to tirass funkcijjas vai klases bez ietvara atkariibam.
- Definejiet portus arejam atkariibam, ko si funkcija izmanto. Izveidojiet saskarnes domena valoda.
- Parvietojiet esooso infrastrukturas kodu adapteru klases, kas ievies sas saskarnes.
- Savienojiet visu kopa kompozicijas sakne, izmantojot atkaribu injekciju.
- Rakstiet testus domenam un lietosanas gadijumiem, izmantojot viltotos adapterus.
Atkartojiet nakamajjai funkcijai. Laika gaita heksagonalas robezas dabiski izplatiissies pa visu koda bazi.
Nobeiguma pardomas
Heksagonala arhitektura nav par stingras veidnes sekosanu. Ta ir par vienu vienkarsu ideju: aizsargajiet savu biznesa logiku no arejas pasaules haosa. Datu bazes mainas. Ietvari zaudee popularitati. API tiek norakstiti. Jusu biznesa noteikumiem vajadzetu to visu parrdziivot.
Modelis stradaa, jo tas atbilst tam, ka programmatura faktiski attiistas. Prasibas mainas pastaavigi. Infrastruktura mainas periodiski. Bet jusu biznesa pamatnoteikumi mueedz but visstabilakaa sistemas dala. Buvejiet ap so stabilitati.
Ja jus buvejat produktu, kam jabut ilgstoosam, heksagonala arhitektura ir viens no labakajiem ieguldijumiem, ko varat veikt sava koda baze.
Vajadziga palidziba lietojumprogrammas arhitekturas projektesana vai refaktoresana? Sazinieties ar mums. Mes paldzam komandam buvet programmaturu, kas paliek uzturama, tai augot.