Zengin Domain Modeli — JPA Entity'de Kapsülleme
Önce şunu oku: Nesne Yönelimli Tasarım — Kapsülleme ve Kompozisyon
30 saniyede özet
Her alanı setter'lı bir nesne ve bütün kuralları bilen bir servis, kuralı veriden ayırır; biri servisi atlarsa kural da atlanır. Kuralı nesnenin kendi metotlarına taşıdığında hangi kapıdan girilirse girilsin atlanamaz.
Bir kuralı yalnızca kapıdaki görevli biliyorsa, arka kapıdan giren onu hiç duymaz.
-
Bayt: Bütün kuralları OrderService'e yazdım. Her şeyi tek yerden yönetmek harika!
-
Sen: Peki gece çalışan toplu iş de o servisi mi kullanıyor?
-
Bayt: Hayır, doğrudan entity'ye yazıyor! Eksi adetli sipariş satırları oluşmuş.
-
Bayt: Demek ki kilit görevlide değil, kasanın kendisinde olmalı.
Spring Boot projelerinin çoğunda aynı ikili vardır: her alanı setter’lı bir @Entity ve bütün kuralları bilen bir OrderService. Çalışır — ta ki biri servisi atlayıp entity’ye doğrudan dokunana kadar.
Kural herkeste, sahibi kimsede
Tanıdık bir kod:
@Entity@Getter @Setterpublic class Order { @Id @GeneratedValue private Long id; @OneToMany(cascade = ALL) private List<OrderLine> lines = new ArrayList<>(); private BigDecimal total = BigDecimal.ZERO; private Status status = Status.NEW;}
@Servicepublic class OrderService { public void addLine(Order order, Product p, int qty, BigDecimal price) { if (qty <= 0) throw new IllegalArgumentException("miktar > 0 olmalı"); order.getLines().add(new OrderLine(p, qty, price)); order.setTotal(order.getTotal().add(price.multiply(BigDecimal.valueOf(qty)))); }}Buna anemik modelYalnızca alan ve getter/setter taşıyan, kuralları başka bir sınıfa (genellikle bir Service) bırakan domain nesnesi. Veri bir yerde, onu koruyan kural başka yerdedir.Sözlükte gör → denir: veri Order’da, onu koruyan kural OrderService’te. Kural ancak çağıran servisten geçerse çalışır.
Sorun şu: getLines() ve setTotal() herkese açık. Başka bir servis, bir batch işi ya da bir test aynı işi servisten geçmeden yapabilir. O zaman miktar kontrolü de toplam hesabı da atlanır.
Kafam karıştı, daha basit anlat
Kural beş ayrı serviste yazılıysa, biri mutlaka unutulur. Kuralın tek bir sahibi olmalı: koruduğu verinin kendisi.
Setter'lı bir @Entity ve bütün kuralları içeren bir OrderService. Bu anemik modelin asıl sorunu nedir?
Satır satır: kuralı entity’ye taşımak
Aynı kural, bu kez koruduğu verinin yanında. Siparişin geçerli kalması için her zaman doğru olması gereken şeylere değişmez kuralNesne var olduğu sürece doğru kalması gereken şey. Kapsüllemenin ölçütü kaç alanın private olduğu değil, hangi kuralın dışarıdan bozulamadığıdır.Bir setter, koruduğu kural varsa onu atlanabilir kılar. Kuralı metodun içine koymak (withdraw gibi) nesneyi geçersiz duruma düşürmeyi imkânsız hâle getirir.Sözlükte gör → denir ve bu kurallar artık yalnızca Order’ın metotlarından geçerek değiştirilebilir.
Bir servis kontrolü unutup `order.addLine(pen, -1, price)` çağırırsa ne olur? Cevabı göster
addLine içindeki kontrol IllegalArgumentException atar ve sipariş olduğu gibi kalır. Kontrolü unutmak artık mümkün değil, çünkü kontrol çağıranda değil, kapının kendisinde.
Adım adım oku
- Anemik modelde kural OrderService'te, yani kapıdaki görevlide durur. Controller'dan gelen istek kontrol edilir.
- Gece çalışan toplu iş ise servisi atlayıp setQuantity ile entity'ye doğrudan yazar: adet eksi bir olur.
- Zengin modelde kural Order'ın kendi addLine metodundadır: kilit kasanın üstündedir.
- Controller'dan da gelse, toplu işten de gelse geçersiz adet reddedilir.
Kural kapıda
@Entitypublic class Order { @OneToMany(cascade = ALL, orphanRemoval = true) private List<OrderLine> lines = new ArrayList<>(); private BigDecimal total = BigDecimal.ZERO; private Status status = Status.NEW; protected Order() {} // yalnızca JPA için public void addLine(Product product, int qty, BigDecimal price) { if (qty <= 0) throw new IllegalArgumentException("miktar > 0 olmalı"); if (status != Status.NEW) throw new IllegalStateException("gönderilmiş sipariş değişmez"); lines.add(new OrderLine(product, qty, price)); total = total.add(price.multiply(BigDecimal.valueOf(qty))); } public List<OrderLine> lines() { return List.copyOf(lines); }}Debug
OrderService Servis kural bilmiyor, isteği Order'a iletiyor: 2 adet kitap, 100 TL.
- qty
- = 2
- price
- = 100
Sol/sağ ok tuşlarıyla da gezebilirsin.
Dikkat: setTotal yok. Toplam hesaplanır, atanmaz — bu yüzden onu satırlardan koparacak bir kod yazmak derlenmez bile.
Kafam karıştı, daha basit anlat
Kontrolü kapının kendisine koy. addLine çağrıldığında sipariş kendi kuralını kontrol eder; kimse bu kontrolü atlayamaz.
Miktar kontrolünü OrderService'ten Order.addLine içine taşıdın. Bu neyi garanti eder?
Zengin Order sınıfında lines() metodu neden List.copyOf(lines) döndürüyor?
JPA ile uyumlu bir zengin entity
Zengin model JPA ile çatışmaz, yalnızca birkaç kurala uyman gerekir:
| JPA’nın istediği | Zengin modeldeki karşılığı |
|---|---|
| Argümansız kurucu | protected Order() {} — Hibernate kullanır, uygulama kodu boş sipariş oluşturamaz |
| Alanlara erişim | Alan erişimi (@Id alanın üstünde). Hibernate setter’a ihtiyaç duymaz |
| Koleksiyonu yönetmek | İç liste private. Dışarıya List.copyOf(lines) |
final olmayan sınıf ve alanlar | Sınıf final değil, değişmezlik metotlarla sağlanır |
Nesneyi oluşturmanın anlamlı yolu da bir fabrika metodu olabilir: Order.placeBy(customerId). Böylece “müşterisi olmayan sipariş” diye bir ara durum hiç oluşmaz.
Sipariş ve satırları birlikte tutarlı kalması gereken bir küme. Bu kümeye aggregateBirlikte tutarlı kalması gereken ve tek bir kök nesne üzerinden değiştirilen nesne kümesi. Dışarıdan yalnızca köke erişilir; başka aggregate'lara id ile referans verilir.Sözlükte gör → denir: dışarıdan yalnızca köke (Order) erişilir, OrderLine’a doğrudan dokunulmaz.
Aynı fikir bir banka hesabında da çok doğal durur. Bakiye ve hesap hareketleri birlikte değişir, kilit de hesabın kendisindedir.
Derinleş · Banka hesabı: kilit hesabın kendisinde 4 dosya · ~110 satır · ilk okumada atlayabilirsin
Zengin bir JPA entity'sinde argümansız kurucu neden protected olmalı?
Bir JPA entity'sine Lombok @Data koymanın riski nedir?
Kendin gör
Aynı sekiz çağrı üç farklı modele karşı çalışıyor. Sağdaki liste siparişin kurallarını, alttaki sayaç kötü bir çağrının nerede durduğunu gösteriyor.
Aynı çağrılar, üç model — kural nerede duruyor?
Tohum 449889Anemik entity + servis çağrı 0/8
order.getLines().add(new OrderLine(book, 2, price));order.setTotal(order.getTotal().add(price.multiply(2)));- Geçerli bir satır eklesırada
- Negatif miktarla satır eklesırada
- Satır listesine dışarıdan eklesırada
- TRY siparişine USD fiyatlı satır eklesırada
- Miktar ile fiyatı yer değiştirerek versırada
- Siparişi göndersırada
- Gönderilmiş siparişi iptal etsırada
- Toplamı elle sıfırlasırada
Siparişin kuralları
- Her satırın miktarı sıfırdan büyük — geçerli
- Toplam, satırların toplamına eşit — geçerli
- Siparişte tek para birimi var — geçerli
- Satır, çağıranın kastettiği değerlerle eklendi — geçerli
- Gönderilmiş sipariş iptal edilemez — geçerli
Kötü çağrı nerede durdu?
- derleme
- 0
- kurucu
- 0
- metot
- 0
- sessizce geçti
- 0
Şu an ne oldu?
Anemik entity + servis — sekiz çağrı sırada
Aynı çağıran kod, sipariş üzerinde sekiz işlem deneyecek. Bazıları geçerli, bazıları hiç izin verilmemesi gereken işlemler.
Aklında kalsın: Bak: her kötü işlem nerede durduruluyor, yoksa hiç durdurulmuyor mu?
Görevler0/3
Anemik modelde üç farklı kuralı sessizce bozaçık
İpucu
Anemik modelle sonuna kadar oynat. Hiçbir işlem hata vermiyor — sorun da tam olarak bu.
Bir kuralın derleme zamanında yakalandığını göraçık
İpucu
Zengin modele geç. Setter’ı olmayan bir alanı atamaya çalışan kod derlenmez.
Sekiz çağrının hiçbiri kuralı bozmadan bitsinaçık
İpucu
Zengin entity tek başına yetmiyor: para birimi ve argüman sırası hâlâ geçiyor. Değerlerin kendisine de tip ver.
Olay günlüğü (0)
Henüz olay yok. Oynat veya adımla.
- Anemik modelle oynat. Hiçbir çağrı hata vermiyor — ve kuralların çoğu kırmızıya dönüyor. Hatasız çalışmak doğru çalışmak demek değil.
- Zengin entity’ye geç. Negatif miktar, dışarıdan liste ve iptal artık metot seviyesinde duruyor.
setTotalhiç derlenmiyor. - Ama iki kural hâlâ kırmızı. Para birimi ve yer değiştirmiş argümanlar geçiyor, çünkü
BigDecimalveintanlam taşımıyor. Bu, bir sonraki dersin konusu: value objectKimliği olmayan, değeriyle eşit sayılan ve değiştirilemeyen küçük nesne: Money, Email, Quantity. Kendi geçerlilik kuralını kurucusunda taşır.Sözlükte gör →. - Görevleri tamamla. Sonuncusu sıfır bozulma istiyor ve bunu yalnızca üçüncü model yapabiliyor.
Kafam karıştı, daha basit anlat
Simülatörde kötü bir çağrının nerede durduğuna bak. Zengin modelde kötü çağrı ilk kapıda durur, veritabanına hiç ulaşmaz.
Simülatörde zengin entity'ye geçtin. Hangi kötü çağrılar yine de sessizce geçiyor?
Tuzaklar
Entity’de Lombok @Data. Her alana setter açar — kapsülleme bir anotasyonla geri alınmış olur. Üstelik lazy koleksiyonlar dahil bütün alanlar üzerinden equals, hashCode ve toString üretir: beklenmedik sorgular, LazyInitializationException ya da çift yönlü ilişkilerde sonsuz özyineleme.
Entity’yi doğrudan JSON’a bağlamak. @RequestBody Order Jackson’ın setter’lara ya da alanlara yazmasını gerektirir ve istemcinin total göndermesine izin verir. İstek için bir record DTO al, entity’yi metotlarla değiştir.
Dev aggregate. Customer içinde bütün siparişleri List<Order> olarak tutmak, bir siparişi değiştirmek için müşteriyi ve tüm siparişlerini yüklemek demek. Aggregate’lar birbirine id ile referans verir.
Servisi yok etmek. Zengin model “servis yok” demek değil.
Servis transaction’ı açar, siparişi yükler, ödeme gibi dış sistemleri çağırır. Yalnızca kural taşımayı bırakır.
Kendini sına
Setter'lı bir entity'de, onu değiştiren her kod kuralları kendisi hatırlamak zorundadır.
Her sorumluluğu ait olduğu yere yerleştir.
Aklında kalacak üç şey
- 1 Setter'lı modelde kuralın sahibi yoktur: Order'ı değiştiren her kod kuralları kendisi hatırlamak zorundadır. Zengin modelde kural, verinin tek kapısında durur.
- 2 Zengin bir JPA entity'si niyet bildiren metotlarla (addLine, cancel), yalnızca çerçeve için protected bir boş kurucuyla ve dışarıya değiştirilemez kopya veren listelerle kurulur.
- 3 Servis yok olmaz, rolü değişir: transaction açar, yükler, dış sistemleri çağırır. Siparişin kendi tutarlılığı ise Order'ın işidir.
5 kart sonraki derste seni bekliyor
Bu dersin üstüne kurulanlar
Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.
- Tasarım & Temiz KodHexagonal Mimari — Bağımlılık Oku Hangi Yöne BakıyorVeritabanını değiştirdin ve iş kuralların da değişmek zorunda kaldı. Ok yanlış yöne mi bakıyor?Derse git
- Tasarım & Temiz KodDomain Olayları — Hesap Açılmadı, Hoş Geldin E-postası Neden Gitti?Müşteri hesap açamadı; ekranda hata gördü. Beş saniye sonra telefonuna "Bankamıza hoş geldiniz" e-postası geldi. Nasıl?Derse git