Value Object ve Record — Anlamı Tipe Taşımak
Önce şunu oku: Zengin Domain Modeli — JPA Entity'de Kapsülleme
30 saniyede özet
Bir ilaç kutusunda yalnızca '5' yazması tehlikelidir: 5 mg mı, 5 tablet mi? long ve BigDecimal de böyle anlamsızdır. Değeri ve kuralını kendi tipine (Money, Quantity) taşıdığında karışıklık derleme hatasına döner.
Bir sayı tek başına pek bir şey anlatmaz; yanında ne olduğu yazmıyorsa herkes onu başka türlü okur.
Adım adım oku
- İlaç kutusunda yalnızca 5 yazıyor: 5 miligram mı, 5 tablet mi, belli değil.
- transfer metodu üç long alıyor. Tutar ile hesap numaralarının yeri karışsa da kod derlenir ve para yanlış yere gider.
- Parametreler AccountId ve Money gibi kendi tiplerine taşınınca her değer ne olduğunu yanında taşır.
- Aynı karışıklık artık derleme hatasıdır: Money, AccountId'nin yerine geçemez.
-
Bayt: Para transferi metodum üç long alıyor: gönderen, alan, tutar.
-
Sen: Çağırırken sırayı karıştırırsam ne olur?
-
Bayt: Kod derlenir ve para yanlış hesaba gider! Derleyici üç long'u birbirinden ayıramaz.
-
Bayt: Sayının yanına ne olduğunu yazınca karışıklık derleme hatasına döner.
Önceki derste kuralları entity’ye taşıdık ve iki çağrı yine de geçti: farklı para birimi ve yer değiştirmiş argümanlar. İkisinin de sebebi aynı: int ve BigDecimal ne olduklarını bilmiyorlar.
Her şey long olunca
Şu imza, Spring Boot servislerinde sık görülür:
public void transfer(long fromAccountId, long toAccountId, BigDecimal amount) { … }
transfer(toId, fromId, amount); // derlenir. Para ters yöne gider.Bu duruma primitive obsessionAnlamı ve kuralı olan bir değeri (para, e-posta, hesap numarası) çıplak bir ilkel tiple (long, String, BigDecimal) taşımak. Derleyici iki long'u birbirinden ayıramaz.Sözlükte gör → denir: anlamı olan bir değeri çıplak bir ilkel tiple taşımak. Derleyici “iki long” görür, “gönderen ve alıcı” görmez.
BigDecimal da aynı sorunu taşır. 15’in lira mı dolar mı olduğunu bilmez, bu yüzden 15 TRY + 5 USD = 20 hesabına hiçbir itirazı olmaz.
Kafam karıştı, daha basit anlat
Her şey long olunca hesap numarasıyla müşteri numarasını karıştırmak çok kolaydır, derleyici de fark etmez. Her birine kendi tipini verirsen karışıklık derlemede yakalanır.
transfer(long fromAccountId, long toAccountId, BigDecimal amount) imzasının asıl riski nedir?
Satır satır: record ile Money
Kimliği olmayan, değeriyle eşit sayılan ve değiştirilemeyen küçük nesneye 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 → denir. Java 21’de bunun doğal kalıbı bir record’dur. Kural ise compact constructorJava record'larında parametre listesi yazılmadan tanımlanan kurucu. Alanlar atanmadan önce çalışır; doğrulama ve normalleştirme için kullanılır.Sözlükte gör →’a yazılır.
`Money.tl(100).equals(new Money(new BigDecimal("100.0"), TRY))` true mu döner, false mu? Cevabı göster
true döner — ama yalnızca compact constructor ölçeği sabitlediği için. BigDecimal.equals değerle birlikte ölçeği de karşılaştırır: 100.0 ile 100.00 ona göre farklıdır. Normalleştirme olmasaydı aynı tutardaki iki Money eşit sayılmazdı.
Kural değerin içinde
public record Money(BigDecimal amount, Currency currency) { public Money { Objects.requireNonNull(currency, "para birimi zorunlu"); if (amount.signum() < 0) throw new IllegalArgumentException("negatif tutar"); amount = amount.setScale(2, RoundingMode.HALF_EVEN); } public static Money tl(long value) { return new Money(BigDecimal.valueOf(value), Currency.getInstance("TRY")); } public Money plus(Money other) { if (!currency.equals(other.currency)) throw new IllegalArgumentException("para birimi uyuşmuyor"); return new Money(amount.add(other.amount), currency); }}Debug
main Money.tl(100) çağrılıyor. Fabrika metodu hem tutarı hem para birimini veriyor.
Sol/sağ ok tuşlarıyla da gezebilirsin.
Record sana equals, hashCode, toString ve değişmez alanları bedava verir. Kuralı vermez. plus içindeki kontrolü yazmazsan, Money sadece iki alanlı bir kılıftır.
Kafam karıştı, daha basit anlat
Value object, iki 5 liralık banknot gibidir: hangisinin elinde olduğu önemli değildir, değerleri aynıysa eşittirler. Üzerlerine yazı yazılıp değiştirilemezler.
record Money(BigDecimal amount, Currency currency) içindeki compact constructor ne işe yarar?
Bu kod ne yazdırır?
Spring Boot’ta saklamak ve taşımak
Value object’in kimliği yok, bu yüzden kendi tablosu da yok. Sahibi olan entity’nin kolonlarında yaşar:
| Durum | Araç | Örnek |
|---|---|---|
| Çok alanlı | @Embeddable + @Embedded | Money → price_amount, price_currency |
| Tek alanlı | AttributeConverter<Email, String> | Email → email kolonu |
| İstek / cevap gövdesi | record DTO + Bean Validation | record PlaceOrder(@NotNull Long productId, @Positive int quantity) |
Hibernate 6.2 ve sonrası (Spring Boot 3.1+) record’ları doğrudan @Embeddable olarak kabul eder. AttributeConverter ise kolondan okurken new Email(...) çağırır; yani veritabanından gelen değer de aynı doğrulamadan geçer.
Controller’da entity yerine record DTO almanın sebebi aynı fikrin öbür yüzü: istemci yalnızca tanımladığın alanları gönderebilir. total ya da status gibi alanları dışarıdan yazmak mümkün olmaz.
Üçünü bir arada görmek için küçük bir havale ekranı düşünelim. IBAN tek alanlı, tutar ise iki alanlı bir value object; ikisi de tabloya kendi kurallarıyla girip çıkıyor.
Derinleş · Havale: IBAN ve Money tabloda ve istekte 4 dosya · ~79 satır · ilk okumada atlayabilirsin
Kafam karıştı, daha basit anlat
Value object’in kendi tablosu olmaz. Sahibinin satırında birkaç kolon olarak yaşar, okunurken yine kendi kurallarından geçer.
Email value object'ini veritabanında tek bir VARCHAR kolonda saklamak istiyorsun. En uygun yol hangisi?
Controller'da @RequestBody olarak entity yerine record bir DTO almanın asıl kazancı nedir?
Kendin gör
Önceki dersin simülatörü, bu kez son iki modeli karşılaştırmak için.
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.
- Modeli
Zengin entityyap ve sonuna kadar oynat. Kuralların beşinden ikisi hâlâ kırmızı: para birimi ve argüman sırası. Zengin entity + value object’e geç. Negatif miktar artık metotta değil,Quantity.of(-1)kurucusunda duruyor. Kural bir adım sola kaydı.- Yer değiştirmiş argümanlara bak. Bu kez kod derlenmiyor:
MoneybirQuantitydeğil. En ucuz hata, hiç çalışmayan koddur. - Sıfır bozulma görevini tamamla.
Simülatörde "Zengin entity + value object" modelinde yer değiştirmiş miktar ve fiyat neden derlenmiyor?
Tuzaklar
BigDecimal.equals ve ölçek. new BigDecimal("2.0").equals(new BigDecimal("2.00")) false döner.
Record’un üretilen equals’ı da bunu kullanır. Ölçeği compact constructor’da sabitle ya da karşılaştırmada compareTo kullan.
Yüzeysel değişmezlik. record Basket(List<Item> items) değişmez görünür ama dışarıdan verilen liste hâlâ değiştirilebilir. Compact constructor’da items = List.copyOf(items); yaz.
Her şeyi sarmalamak. Kuralı ya da karıştırılma riski olmayan bir alan (serbest bir “not”) için value object yalnızca dönüştürme kodu ekler. Soru şu: bu değerin bir kuralı var mı, başka bir değerle karışabilir mi?
Value object’e kimlik vermek. Money’yi @Entity yapıp id vermek onu bir entity’ye çevirir. İki aynı tutar artık “farklı” nesneler olur.
Kendini sına
İki long parametrenin yeri karışırsa derleyici hata verir.
Value object ne zaman gereksiz karmaşa olur?
Aklında kalacak üç şey
- 1 Her şey long ve String olunca derleyici anlamı göremez: iki long'u yer değiştirmek derlenir. Money, Quantity, AccountId gibi tipler bu karışıklığı derleme hatasına çevirir.
- 2 Record bir value object için doğru kalıptır ama tek başına yetmez. Kural record'un kurucusunda ve plus gibi metotlarda yazılmalıdır.
- 3 Spring Boot'ta çok alanlı value object @Embeddable, tek alanlı olan AttributeConverter ile saklanır. İstek gövdesi entity değil, record bir DTO olur.
5 kart sonraki derste seni bekliyor
Bu dersin üstüne kurulanlar
Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.