Hexagonal Mimari — Bağımlılık Oku Hangi Yöne Bakıyor
Önce şunu oku: SOLID — Beş Harf, Tek Soru , Zengin Domain Modeli — JPA Entity'de Kapsülleme
30 saniyede özet
Controller, Service, Repository dizilimi iş kuralını veritabanına bağlar. İş kuralı kendi ihtiyacını bir arayüzle tanımlarsa ok tersine döner: veritabanı değişse de kural değişmez, tıpkı prize göre değişen şarj adaptörü gibi.
Yurt dışına çıkınca bilgisayarını değiştirmezsin, yalnızca fişin ucundaki adaptörü değiştirirsin. Yazılımda ise çoğu zaman bilgisayarı prize lehimleriz.
Adım adım oku
- İş kuralı JpaRepository'yi doğrudan import ediyor: bilgisayar prize lehimlenmiş.
- Veritabanı değişince iş kuralının kodu da değişmek zorunda kalır.
- İş kuralı kendi ihtiyacını bir arayüzle, yani port ile tanımlar. JPA adaptörü bu portu uygular.
- Veritabanı MongoDB olunca yalnızca adaptör değişir; iş kuralına dokunulmaz.
-
Bayt: Veritabanını değiştirmek istedik. Sadece repository'ler değişecek sanıyordum.
-
Sen: Değişmedi mi?
-
Bayt: İş kurallarının yarısı da değişti! Hepsi JPA sınıflarını import ediyormuş.
-
Bayt: Bilgisayar prizi tanımamalı, kendi girişini tanımalı. Oku çevirelim.
Spring Boot projelerinin çoğu aynı üç klasörle başlar: controller, service, repository. Düzenli görünür. Ama service içindeki iş kuralı, bir JpaRepository’yi import ettiği anda verinin nasıl saklandığını bilmeye başlar.
Ok nereye bakıyor?
Klasik yapıda istek yukarıdan aşağı akar ve bağımlılıklar da aynı yöne bakar:
package com.shop.application;
import com.shop.infrastructure.OrderJpaRepository; // JpaRepository<Order, Long>
@Servicepublic class PlaceOrder { private final OrderJpaRepository orders; …}Bu import bir oktur: application → infrastructure. Ok değişikliğin yayılacağı yönü gösterir. Veritabanını, sorgu stratejisini ya da Spring Data sürümünü değiştirdiğinde iş kuralını da açman gerekebilir.
Test tarafında da bedeli var. PlaceOrder’ı sınamak için ya bir veritabanı ayağa kaldırırsın ya da JpaRepository’nin onlarca metodunu taklit edersin.
Kafam karıştı, daha basit anlat
İş kuralın veritabanını tanıyorsa, veritabanı değiştiğinde iş kuralın da değişmek zorunda kalır. Bu okun yönü yanlıştır.
PlaceOrder use case'i com.shop.infrastructure.OrderJpaRepository'yi import ediyor. Bu bağımlılık oku ne anlatır?
Priz ve adaptör: oku çevirmek
hexagonal mimariİş kuralını merkeze koyup dış dünyayla (veritabanı, HTTP, mesaj kuyruğu) yalnızca arayüzler üzerinden konuşan mimari. Bağımlılıklar her zaman merkeze doğru bakar.Sözlükte gör → iş kuralını merkeze koyar. Merkez, dış dünyadan ne beklediğini kendi arayüzleriyle tanımlar. Bu arayüzlere ve onları uygulayan sınıflara port ve adapterPort, iş kuralının dış dünyadan beklediğini tanımlayan arayüzdür ve iş kuralının paketinde durur. Adapter, onu belirli bir teknolojiyle (JPA, REST, Kafka) uygulayan sınıftır.Sözlükte gör → denir:
// com.shop.domain — iş kuralının dilipublic interface OrderRepository { Order save(Order order); Optional<Order> findById(OrderId id);}
// com.shop.infrastructure — teknolojinin diliinterface OrderJpaRepository extends JpaRepository<OrderEntity, Long> {}
@Componentclass JpaOrderAdapter implements OrderRepository { private final OrderJpaRepository jpa; public Order save(Order order) { return jpa.save(OrderEntity.from(order)).toDomain(); } …}Artık PlaceOrder yalnızca OrderRepository’yi tanıyor. Ok infrastructure → domain yönünde: altyapı iş kuralına uyuyor, tersi değil. Bu, bağımlılık tersine çevirmeİş katmanının neye ihtiyaç duyduğunu kendi tanımlaması, altyapının ona uyması. Arayüzün varlığı değil, hangi katmana ait olduğu belirleyicidir.Sözlükte gör → ilkesinin paket düzeyindeki hâlidir.
Spring tarafında ek bir şey gerekmez. PlaceOrder kurucusunda OrderRepository ister, Spring port’u uygulayan tek bean’i, yani adapter’ı enjekte eder.
Bir banka havalesinde görelim: testte port’u küçük bir sahte sınıfla değiştiriyoruz.
Derinleş · Hesaplar arası havale: port ve adapter 4 dosya · ~72 satır · ilk okumada atlayabilirsin
Kafam karıştı, daha basit anlat
Merkez “bana şu şekilde bir priz lazım” der. Veritabanı ya da dış servis, o prize uyan bir fiş takar. Böylece fişi değiştirmek merkezi hiç etkilemez.
Hexagonal mimaride OrderRepository arayüzü (port) hangi pakette durmalı?
PlaceOrder kurucusunda OrderRepository (port) istiyor. Spring hangi nesneyi verir?
Satır satır: kuralı ArchUnit ile korumak
Paket kuralını yazıya dökmek yetmez. İlk acele eden kişi domain’e bir JPA import’u ekler ve kimse fark etmez. Kural bir testte durmalı.
domain paketindeki Order sınıfında yalnızca @Entity anotasyonu var, hiçbir JPA sınıfı çağrılmıyor. Aşağıdaki kural bunu ihlal sayar mı? Cevabı göster
Sayar. ArchUnit bytecode’u inceler ve bir sınıfın taşıdığı anotasyonu da o anotasyonun paketine bir bağımlılık olarak görür. @Entity, jakarta.persistence’a bir oktur.
Mimari bir birim testi
@AnalyzeClasses(packages = "com.shop")class ArchitectureTest { @ArchTest static final ArchRule domainIsPure = noClasses().that().resideInAPackage("..domain..") .should().dependOnClassesThat() .resideInAnyPackage("jakarta.persistence..", "org.springframework.."); @ArchTest static final ArchRule useCasesDoNotSeeInfrastructure = noClasses().that().resideInAPackage("..application..") .should().dependOnClassesThat().resideInAPackage("..infrastructure..");}Debug
ArchUnit com.shop altındaki bütün sınıfların bytecode'u okundu. Uygulama çalıştırılmadı; bu saf bir birim testi.
Sol/sağ ok tuşlarıyla da gezebilirsin.
İlk kural kırmızı. Ya domain’deki @Entity’yi ayrı bir OrderEntity’ye taşırsın ya da kuralı bilinçli olarak gevşetirsin.
İkisi de meşru. Önemli olan, kararın testte görünür olması.
Kafam karıştı, daha basit anlat
Kuralı bir wiki sayfasına yazmak yetmez, biri unutur. ArchUnit, “merkez veritabanını tanımasın” kuralını bir teste çevirir ve ihlal edildiği gün kırmızı yanar.
Paket kurallarını bir wiki sayfasına yazmak yerine ArchUnit testine koymanın kazancı nedir?
noClasses().that().resideInAPackage("..domain..").should().dependOnClassesThat().resideInAnyPackage("jakarta.persistence..") kuralı, domain'deki Order sınıfının @Entity anotasyonunu yakalar mı?
Kendin gör
Üç kararı değiştir, adımlayarak kodu, bağımlılık grafiğini, ArchUnit sonucunu ve veritabanı değişikliğinin etkisini gör.
Paket bağımlılıkları — ok hangi yöne bakıyor?
Tohum 790363package com.shop.application; import com.shop.domain.Order;import com.shop.infrastructure.OrderJpaRepository; // JpaRepository<Order, Long> @Servicepublic class PlaceOrder { private final OrderJpaRepository orders; public OrderId place(PlaceOrderCommand cmd) { Order order = Order.placeBy(cmd.customerId()); return orders.save(order).id(); }}Şu an ne oldu?
PlaceOrder use case'i ve import satırları
Bağımlılıklar import satırlarında başlar. Adımla ve bu satırların paketler arasında hangi okları çizdiğini gör.
Görevler0/3
Bir ArchUnit kuralının kırmızıya döndüğünü göraçık
İpucu
Varsayılan ayarlarla kurallar aşamasına kadar ilerle.
Üç kuralı da geçir ve veritabanı değişikliğini tek pakete indiraçık
İpucu
Repository arayüzünü domain'e taşı, JPA anotasyonlarını domain'den çıkar, controller'ı use case'ten geçir.
Bilinçli ödün: domain'de JPA kalsın ama use case altyapıyı hiç görmesinaçık
İpucu
Pek çok ekip @Entity'yi domain'de bırakır. Port'u yine de domain'e koy ve bedelini oku.
Olay günlüğü (0)
Henüz olay yok. Oynat veya adımla.
- Varsayılanla adımla. Repository infrastructure’da, entity’lerde JPA var. Grafikte iki kırmızı ok, veritabanını değiştirmek üç pakete dokunuyor.
- Repository’yi port’a çevir.
application → infrastructureoku kayboluyor, yerineinfrastructure → domaingeliyor. Use case artık sahte bir repository ile test edilebiliyor. - Domain’deki JPA’yı kapat. Üç kural da yeşil, veritabanı değişikliği tek pakete indi.
- Controller’ın repository’yi doğrudan çağırmasını aç. Tek bir kısayol, temiz grafiği yeniden bozuyor. Üç görevi de tamamla; sonuncusu bilinçli bir ödün.
Simülatördeki "bilinçli ödün": port domain'de, ama JPA anotasyonları da domain'de. Sonuç ne olur?
Tuzaklar
CRUD servise hexagonal giydirmek. Korunacak bir iş kuralı yoksa her tablo için port, adapter ve iki model arasında dönüşüm yalnızca dosya sayısını artırır. Spring Data repository’yi doğrudan kullanmak dürüst bir seçimdir.
İki model, iki kat bakım. Domain’de saf Order, infrastructure’da OrderEntity tutmak domain’i JPA’dan tamamen kurtarır ama her alan için bir dönüşüm yazmayı gerektirir.
Pek çok ekip @Entity’yi domain’de bırakır ve yalnızca port’u uygular. Bu bir ödündür; kuralı da buna göre yaz.
Katmana göre paketlemek. controller/, service/, repository/ klasörleri bir özelliği dört yere dağıtır.
Özelliğe göre paketle (order/, payment/) ve katmanları her özelliğin içine koy. Sınırlar paket sınırı olur; Spring Modulith ya da ArchUnit ile doğrulanabilir.
Her şey için bir port. Her use case için ayrı bir giriş port’u, her tablo için ayrı bir çıkış port’u açmak, kalıbı törene çevirir. Port bir ihtiyaçtır; ihtiyaç yoksa port da yoktur.
Kendini sına
İş kuralı bir JpaRepository'yi import ederse veritabanı değişikliği kurala kadar ulaşır.
Hangi serviste port/adapter katmanı büyük olasılıkla gereksiz karmaşa olur?
Aklında kalacak üç şey
- 1 Bağımlılık oku, bir değişikliğin nereye yayılacağını gösterir. İş kuralı bir JpaRepository'yi import ediyorsa, veritabanı değişikliği kurala kadar ulaşır.
- 2 Port, iş kuralının ihtiyacını tanımlayan arayüzdür ve onu kullanan tarafta durur. Adapter onu altyapı tarafında uygular.
- 3 Mimari kural bir wiki sayfasında değil, ArchUnit testinde durmalı. Bu yaklaşım da her yerde gerekmez: basit CRUD servislerinde fazladan yüktür.
5 kart sonraki derste seni bekliyor