Çoğu kod tabanı merkezde framework ile başlar: controller'lar servisleri, servisler ORM'i çağırır ve iş kuralları bu üçünün hepsine sızar. Böyle olunca bir fiyatlandırma kuralını test etmek bir web sunucusu ve bir veritabanı gerektirir; ödeme sağlayıcısını değiştirmek ise bir siparişin ne kadar tuttuğuna karar veren koda dokunmak demektir.
Port'lar ve adapter'lar olarak da bilinen altıgen mimari (hexagonal architecture) bunu tersine çevirir. Bu yazı fikri ve terimleri (port'lar, sürücü ve sürülen adapter'lar, bağımlılık kuralı) açıklıyor, bir sipariş servisi için diyagram gösteriyor ve desenin ek dosyalarına ne zaman değdiğini ele alıyor.
Tek cümlede fikir
İş mantığını HTTP'den, veritabanlarından ya da tedarikçilerden hiçbir şey bilmeyen bir çekirdeğe koyun ve geri kalan her şeyi ona, sahibi çekirdek olan arayüzler üzerinden bağlayın.
Alistair Cockburn bu deseni 2005'te özgün altıgen mimari makalesinde tanımladı. Belirtilen amacı, bir uygulamanın kullanıcılar, programlar, otomatik testler ya da toplu iş betikleri tarafından eşit şekilde sürülebilmesi ve sonunda kullanacağı veritabanlarından ve cihazlardan bağımsız olarak geliştirilip test edilebilmesidir. Altıgen şeklin, kenarlarına birkaç port çizmeye yer bırakmak dışında bir anlamı yoktur.
Terimler
| Terim | Nedir | Sipariş servisi örneği |
|---|---|---|
| Çekirdek | Use case'ler ve domain modeli | PlaceOrder, Order, Money, fiyatlandırma kuralları |
| Gelen (sürücü) port | Çekirdeğin dışarıya sunduğu bir arayüz | PlaceOrder.execute(command) |
| Giden (sürülen) port | Çekirdeğin dışarıdan ihtiyaç duyduğu bir arayüz | OrderRepository, PaymentGateway |
| Sürücü adapter | Dışarıdan gelen girdiyi port çağrılarına çevirir | REST controller, CLI, mesaj tüketicisi, test |
| Sürülen adapter | Bir giden port'u gerçek teknolojiyle uygular | Postgres repository, ödeme API istemcisi |
Hepsini birbirine bağlayan şey bağımlılık kuralıdır: kaynak kod bağımlılıkları içeri doğru işaret eder. Adapter'lar port'lara bağımlıdır; port'lar ve çekirdek ise altıgenin dışındaki hiçbir şeye bağımlı değildir. Çalışma zamanında çekirdek ödeme ağ geçidini çağırır, ama kodda ödeme adapter'ı çekirdeğin arayüzüne bağımlıdır, tersi değil.
Uçtan uca bir örnek
Sipariş vermek, siparişi kaydetmeyi ve müşteriden ödeme almayı gerektirir. Çekirdekte:
// ports, owned by the core
interface OrderRepository { save(order: Order): Promise<void> }
interface PaymentGateway { authorize(amount: Money, token: string): Promise<AuthResult> }
// use case
class PlaceOrder {
constructor(private orders: OrderRepository, private payments: PaymentGateway) {}
async execute(cmd: PlaceOrderCommand): Promise<OrderId> {
const order = Order.create(cmd.items, cmd.customerId) // domain rules
const auth = await this.payments.authorize(order.total(), cmd.paymentToken)
if (!auth.ok) throw new PaymentDeclined(auth.reason)
await this.orders.save(order.markPaid(auth.id))
return order.id
}
}
Çekirdeğin dışında PostgresOrderRepository, OrderRepository arayüzünü SQL ile; PaymentApiGateway ise PaymentGateway arayüzünü HTTP çağrıları, yeniden denemeler ve hata eşlemesiyle uygular. Bir REST controller isteği ayrıştırır, bir PlaceOrderCommand oluşturur ve use case'i çağırır. Bir test de aynısını bellek içi adapter'larla yapar; web sunucusu ya da veritabanı gerekmez.
Klasörlere eşlemek
Klasörler yapıyı açıkça söylediğinde onu korumak daha kolaydır:
src/
core/
domain/ Order, Money, pricing rules
usecases/ PlaceOrder, CancelOrder
ports/ OrderRepository, PaymentGateway
adapters/
http/ REST controllers (driving)
postgres/ repository implementations (driven)
payments/ payment API client (driven)
main.ts wiring: builds adapters and passes them to use cases
core/ altındaki herhangi bir şey adapters/ klasöründen ya da bir framework paketinden import yaptığında build'i başarısız kılan bir lint kuralı ya da mimari testi ekleyin. Bu kural kazara kolayca bozulur ve otomatik olarak uygulanması ucuzdur.
Sık yapılan hatalar
- Teknolojiye göre şekillenmiş port'lar.
OrderRepository.executeQuery(sql), adı değiştirilmiş bir veritabanı API'sidir. Port'ları çekirdeğin neye ihtiyaç duyduğuna göre adlandırın:save,findOpenOrdersFor(customer). - ORM entity'si olan domain nesneleri. Kalıcılık framework'ünün anotasyonları veritabanını çekirdeğin içine sürükler. Domain nesneleri ile kalıcılık modelleri arasındaki eşlemeyi adapter'da yapın.
- İş kuralı barındıran adapter'lar. 500 doların üzerindeki iadelerin onay gerektirdiğine karar veren bir ödeme adapter'ı, bir kuralı çekirdeğin dışına taşımış olur. Adapter'ları çeviri ve taşıma ile sınırlı tutun.
- Her sınıf için bir port. Port'lar sınırlar için vardır, çekirdeğin içindeki her iş birlikçi için değil.
Mimari diyagram
Diyagram içeriği
- PlaceOrder use case
- Domain model
- PlaceOrder port
- OrderRepository port
- PaymentGateway port
- REST controller
- Test driver
- Postgres adapter
- Payment API adapter
- Postgres
- Payment provider
- calls
- implements
- SQL
- HTTPS
Nasıl okunur:
- Altıgen uygulama çekirdeğidir: PlaceOrder use case'i ve domain modeli; framework ya da tedarikçi kodu yoktur.
- Sol, sürücü taraf: bir REST controller ve bir test sürücüsü, gelen PlaceOrder port'unu çağırır. Çekirdeğin bakış açısından birbirlerinin yerine geçebilirler.
- Sağ, sürülen taraf: use case iki giden port'u çağırır. İçi boş ok uçlu kesikli kenarlar, bunları uygulayan Postgres adapter'ını ve ödeme API adapter'ını gösterir; bu, kod bağımlılığının yönüdür.
- En sağ: gerçek Postgres veritabanı ve ödeme sağlayıcısı; bunları yalnızca adapter'lar bilir.
- Markdown bloğu bağımlılık kuralını belirtir; inceleme yapanların kontrol etmesi gereken tek şey budur.
Testler ucuzlar
Faydanın çoğu testlerde ortaya çıkar:
- Çekirdek testleri bellek içi adapter'lar kullanır ve milisaniyeler içinde çalışır. Fiyatlandırma kuralları, durum geçişleri ve hata yönetimi I/O olmadan test edilir.
- Adapter testleri tek bir adapter'ı gerçek teknolojiye karşı sınar: Postgres repository'sini gerçek bir Postgres'e, ödeme adapter'ını sağlayıcının sandbox ortamına karşı.
- Birkaç uçtan uca test bağlantıların doğru kurulduğunu kanıtlar.
Bu ayrım test piramidinin somut halidir ve ekiplerin bu deseni küçük servislerde bile benimsemesinin nedeni de budur.
Yeni bir giriş yolu eklemek
Gelen port, ikinci bir kanal geldiği ilk anda karşılığını verir. Diyelim ki işletme, web'in yanı sıra bir iş ortağının mesaj kuyruğundan da sipariş almak istiyor. Bir mesajı okuyan, aynı PlaceOrderCommand'ı oluşturan ve aynı port'u çağıran bir tüketici yazarsınız. Doğrulama, fiyatlandırma ve ödeme mantığı değişmez; testleri de değişmez. Port olmadan genellikle tüketicinin içinde sipariş mantığının ikinci bir kopyası oluşur ve birkaç sürüm içinde ilkinden uzaklaşır.
Değmediği durumlar
- Kuralsız CRUD. Servis çoğunlukla HTTP ile bir tablo arasında veri taşıyorsa port'lar yalnızca seremonidir.
- Kısa ömürlü kod. Prototipler ve betikler ek arayüzlerin karşılığını vermez.
- Sonsuza dek tek adapter. Veritabanını asla değiştirmeyecekseniz ve zaten gerçek bir veritabanına karşı test ediyorsanız, bir giden port az getirisi olan bir dolaylılık ekler. Yine de çekirdeği framework kodundan arınmış tutun.
Altıgen mimari, onion ve clean architecture ile yakın akrabadır. Katman terminolojileri farklıdır, ama bağımlılık kuralını paylaşırlar.
Diyagrama neler konmalı
- Çekirdeğin sınırı, yalnızca domain'i ve use case'leri açıkça çevreleyen bir şekil olarak çizilmiş.
- Sınırın üzerindeki port'lar, çekirdeğin onları adlandırdığı şekilde adlandırılmış.
- Dışarıdaki adapter'lar, sürücü ya da sürülen olarak ve sardıkları teknolojiyle etiketlenmiş.
- Sürülen adapter'lar için bağımlılık yönü, çağrı yönünden farklı olarak.
Tek bir servis yerine bütün bir sistem için C4 modeli diyagram örneği container'lardan bileşenlere nasıl yakınlaşılacağını, mikroservis mimari diyagramı ise bunun gibi servislerin nereye oturduğunu gösteriyor. Ödeme ağ geçidi entegrasyon diyagramı ödeme adapter'ının arkasında olanları ele alıyor.
Archboard'da oluşturun
- Google ya da GitHub ile giriş yapın ve bir diyagram oluşturun.
- Uygulama çekirdeği için Temel paketinden bir Altıgen şekli ekleyin ve bir Metin etiketi koyun.
- İçine, use case ve domain modeli için C4 Modeli paketinden Bileşen düğümleri ekleyin; üç port için de sınırın üzerine Bileşen düğümleri yerleştirin.
- Dışarıya sürücü ve sürülen adapter'lar için Konteyner düğümleri, ayrıca Veri paketinden Postgres ve ödeme sağlayıcısı için bir Harici sistem ekleyin.
- Gerçekleme (implementation) kenarlarını içi boş ok uçlu kesikli çizgilerle çizin ve bağımlılık kuralını içeren, Temel paketinden bir Markdown bloğu ekleyin.
Örnek 13 diyagram düğümü kullanıyor ve Free planın sınırları içinde kalıyor.
SSS
Port ile adapter arasındaki fark nedir? Port, sahibi çekirdek olan bir arayüzdür. Adapter ise bu arayüzü bir REST framework'ü ya da bir veritabanı gibi belirli bir teknolojiye bağlayan koddur.
Sürücü ve sürülen adapter'lar nedir? Sürücü adapter'lar çekirdekle etkileşimi başlatır: controller'lar, tüketiciler ve testler gibi. Sürülen adapter'ları ise çekirdek çağırır: repository'ler ve API istemcileri gibi.
Altıgen mimari, clean architecture ile aynı şey mi? Bağımlılık kuralını paylaşırlar: bağımlılıklar domain'e doğru işaret eder. Clean ve onion architecture çekirdeğin içine adlandırılmış katmanlar ekler; altıgen mimari ise yalnızca içeriyi dışarıdan ayırır.
Her servisin altıgen mimariye ihtiyacı var mı? Hayır. Gerçek iş kurallarının ve onları sürmek ya da saklamak için birden fazla yolun olduğu yerde karşılığını verir. İnce CRUD servislerinde çoğunlukla fazladan dosya demektir.
Transaction'lar nereye konur? Genellikle use case'e ya da bir unit-of-work port'una; böylece neyin birlikte commit edilmesi gerektiğine çekirdek, nasıl yapılacağına adapter karar verir.