DDD OOP ile tanıştı: Nesneye yönelik bir depo nasıl uygulanır?


12

Bir DDD deposunun tipik bir uygulaması çok OO'ya benzemez, örneğin bir save()yöntem:

package com.example.domain;

public class Product {  /* public attributes for brevity */
    public String name;
    public Double price;
}

public interface ProductRepo {
    void save(Product product);
} 

Altyapı bölümü:

package com.example.infrastructure;
// imports...

public class JdbcProductRepo implements ProductRepo {
    private JdbcTemplate = ...

    public void save(Product product) {
        JdbcTemplate.update("INSERT INTO product (name, price) VALUES (?, ?)", 
            product.name, product.price);
    }
} 

Böyle bir arayüz Producta'nın en azından alıcılarla birlikte anemik bir model olmasını bekler .

Öte yandan OOP, bir Productnesnenin kendini nasıl kurtaracağını bilmesi gerektiğini söylüyor .

package com.example.domain;

public class Product {
    private String name;
    private Double price;

    void save() {
        // save the product
        // ???
    }
}

Zaman şey, Productkendisini kurtarmak için nasıl bilir o infstrastructure kod alanı kodundan ayrı değil demektir.

Belki de tasarrufu başka bir nesneye devredebiliriz:

package com.example.domain;

public class Product {
    private String name;
    private Double price;

    void save(Storage storage) {
        storage
            .with("name", this.name)
            .with("price", this.price)
            .save();
    }
}

public interface Storage {
    Storage with(String name, Object value);
    void save();
}

Altyapı bölümü:

package com.example.infrastructure;
// imports...

public class JdbcProductRepo implements ProductRepo {        
    public void save(Product product) {
        product.save(new JdbcStorage());
    }
}

class JdbcStorage implements Storage {
    private final JdbcTemplate = ...
    private final Map<String, Object> attrs = new HashMap<>();

    private final String tableName;

    public JdbcStorage(String tableName) {
        this.tableName = tableName;
    }

    public Storage with(String name, Object value) {
        attrs.put(name, value);
    }
    public void save() {
        JdbcTemplate.update("INSERT INTO " + tableName + " (name, price) VALUES (?, ?)", 
            attrs.get("name"), attrs.get("price"));
    }
}

Bunu başarmak için en iyi yaklaşım nedir? Nesne yönelimli bir havuz uygulamak mümkün müdür?


6
OOP, bir Ürün nesnesinin kendini nasıl kurtaracağını bilmesi gerektiğini söylüyor - bunun gerçekten doğru olduğundan emin değilim ... OOP kendi başına bunu gerçekten dikte etmiyor, daha çok bir tasarım / desen problemi (DDD / her neyse
-use

1
OOP bağlamında, nesnelerden bahsettiğini unutmayın. Veri kalıcılığı değil, sadece nesneler. İfadeniz, bir nesnenin durumunun kabul ettiğim kendi dışında yönetilmemesi gerektiğini gösterir. Bir depo (OOP alanı dışında olan) bazı kalıcılık katmanından yükleme / tasarruftan sorumludur. Sınıf özellikleri ve yöntemleri kendi bütünlüklerini korumalıdır, evet, ancak bu durum başka bir nesnenin devleti sürdürmekten sorumlu olamayacağı anlamına gelmez. Alıcılar ve ayarlayıcılar, nesnenin gelen / giden verilerinin bütünlüğünü sağlamak içindir.
jleach

1
"Bu, başka bir nesnenin devlete devam etmekten sorumlu olamayacağı anlamına gelmez." - Bunu söylemedim. Önemli ifade, bir nesnenin aktif olması gerektiğidir . Bu, nesnenin (ve hiç kimsenin) bu işlemi başka bir nesneye devredemeyeceği, ancak bunun tersi olmayacağı anlamına gelir: hiçbir nesne, kendi bencil çalışmasını işlemek için pasif bir nesneden bilgi toplamamalıdır (bir repo alıcılarla yapacağı gibi) . Bu yaklaşımı yukarıdaki parçacıklarda uygulamaya çalıştım.
ttulka

1
@jleach Haklısınız, OOP anlayışımız farklı, benim için alıcılar + pasifler hiç OOP değil, aksi takdirde sorumun bir anlamı yoktu. Yine de teşekkürler! :-)
ttulka

1
İşte benim açımdan bir makale: martinfowler.com/bliki/AnemicDomainModel.html Anemik modeli her durumda againt değilim, örneğin fonksiyonel programlama için iyi bir stratejidir. OOP değil.
ttulka

Yanıtlar:


7

Sen yazdın

Öte yandan OOP, bir Product nesnesinin kendini nasıl kurtaracağını bilmesi gerektiğini söylüyor

ve bir yorumda.

... onunla yapılan tüm operasyonlardan sorumlu olmalı

Bu yaygın bir yanlış anlamadır. Productbir etki alanı nesnesidir, bu nedenle tek bir ürün nesnesini içeren etki alanı işlemlerinden sorumlu olmalıdır , daha az değil, artık değil - bu yüzden kesinlikle tüm işlemler için değil . Genellikle kalıcılık bir etki alanı işlemi olarak görülmez. Tam tersine, kurumsal uygulamalarda, etki alanı modelinde (en azından belirli bir dereceye kadar) kalıcılık bilgisizliği elde etmeye çalışmak nadir değildir ve kalıcılık mekaniğini ayrı bir havuz sınıfında tutmak bunun için popüler bir çözümdür. "DDD" bu tür uygulamaları amaçlayan bir tekniktir.

Öyleyse a için mantıklı bir alan adı operasyonu ne olabilir Product? Bu aslında uygulama sisteminin etki alanı bağlamına bağlıdır. Sistem küçükse ve yalnızca CRUD işlemlerini destekliyorsa, gerçekten de, Productörneğin örnekte olduğu gibi oldukça "anemik" kalabilir. Bu tür uygulamalar için, veritabanı işlemlerini ayrı bir repo sınıfına koymak ya da hiç DDD kullanmak güçlük çekiyorsa tartışmalı olabilir.

Bununla birlikte, uygulamanız ürünleri satın alma veya satma, stokta tutma ve yönetme veya onlar için vergi hesaplama gibi gerçek iş işlemlerini desteklediği anda, bir Productsınıfa makul şekilde yerleştirilebilecek işlemleri keşfetmeye başlamanız oldukça yaygındır . Örneğin, CalcTotalPrice(int noOfItems)hacim indirimleri dikkate alındığında `belirli bir ürünün` n kaleminin fiyatını hesaplayan bir işlem olabilir .

Kısacası, sınıflar tasarlarken, Joel Spolsky'nin beş dünyasından hangisi olduğunuzu ve sistem yeterli alan mantığı içeriyorsa DDD'nin faydalı olacağını düşünmelisiniz. Cevap evet ise, kalıcılık mekaniğini etki alanı sınıflarının dışında tuttuğunuz için anemik bir model elde etmeniz pek olası değildir.


Demek istediğin bana çok mantıklı geliyor. Böylece, ürün, anemik veri yapıları (veritabanı) bağlamının sınırını geçerken anemik bir veri yapısı haline gelir ve depo bir ağ geçididir. Ancak bu, daha sonra API'sının bir parçası haline gelen ve kalıcılık ile ilgisi olmayan diğer kodlar tarafından kolayca kötüye kullanılabilecek alıcı ve ayarlayıcılar aracılığıyla nesnenin iç yapısına erişim sağlamak zorunda olduğum anlamına geliyor. Bundan kaçınmak için iyi bir uygulama var mı? Teşekkür ederim!
ttulka

"Ancak bu yine de alıcı ve ayarlayıcılar aracılığıyla nesnenin iç yapısına erişim sağlamak zorunda olduğum anlamına geliyor ." Kalıcılığı bilmeyen bir etki alanı nesnesinin iç durumu genellikle yalnızca etki alanıyla ilgili bir dizi özellik tarafından verilir. Bu öznitelikler için alıcılar ve ayarlayıcılar (veya bir yapıcı başlatma) mevcut olmalıdır, aksi takdirde "ilginç" alan adı işlemi mümkün olmaz. Bazı çerçevelerde, yansıma yoluyla özel niteliklerin devam etmesini sağlayan kalıcılık özellikleri de vardır, bu nedenle kapsülleme sadece "diğer kod" için değil, sadece bu mekanizma için kırılmıştır.
Doc Brown

1
Kalıcılığın genellikle alan adı işlemlerinin bir parçası olmadığını, ancak buna ihtiyaç duyan nesnenin içindeki "gerçek" alan adı işlemlerinin bir parçası olması gerektiğini kabul ediyorum. Örneğin Account.transfer(amount)aktarımı devam ettirmelidir. Bunu nasıl gerçekleştirdiği, bazı dış varlıkların değil, nesnenin sorumluluğudur. Öte yandan nesnenin ekrana olduğunu genellikle etki alanı operasyon! Gereksinimler genellikle şeylerin nasıl görünmesi gerektiğini ayrıntılı olarak açıklar. Proje üyeleri arasında, iş dünyasında veya başka bir yerde dilin bir parçasıdır.
Robert Bräutigam

@ RobertBräutigam: klasik Account.transfergenellikle iki hesap nesnesi ve bir iş nesnesi içerir. İşlemsel kalıcı işlem daha sonra ikincisinin bir parçası olabilir (ilgili depolara çağrılarla birlikte), bu nedenle "transfer" yönteminin dışında kalır. Bu şekilde, Accountkalıcılık-cahil kalabilir. Bunun sözde çözümünüzden mutlaka daha iyi olduğunu söylemiyorum, ama sizinki de birkaç olası yaklaşımdan sadece biri.
Doc Brown

1
@ RobertBräutigam Nesne ve masa arasındaki ilişki hakkında çok fazla düşündüğünüzden emin olabilirsiniz. Nesneyi kendisi için, hepsi bellekte bir durum olarak düşünün. Hesap nesnelerinizdeki aktarımları yaptıktan sonra, yeni duruma sahip nesneleriniz kalır. Devam etmek istediğiniz şey budur ve neyse ki hesap nesneleri durumları hakkında size bilgi vermek için bir yol sağlar. Bu, durumlarının veritabanındaki tablolara eşit olması gerektiği anlamına gelmez - yani aktarılan tutar, ham tutarı ve para birimini içeren bir para nesnesi olabilir.
Steve Chamaillard

5

Pratik teorinin üstesinden gelir.

Deneyim bize Product.Save () yönteminin birçok soruna yol açtığını öğretir. Bu problemleri aşmak için depo modelini icat ettik.

Ürün verilerini gizlemek için OOP kuralını ihlal ettiğinden emin olun. Ama iyi çalışıyor.

Her şeyi kapsayan bir dizi tutarlı kural yapmak, istisnaları olan bazı genel iyi kurallar yapmaktan daha zordur.


3

DDD OOP ile buluşuyor

Bu iki fikir arasında gerilim olması amaçlanmadığını akılda tutmaya yardımcı olur - değer nesneleri, agregalar, depolar, bazıları OOP'un doğru yapıldığını düşündüğü şeydir.

Öte yandan OOP, bir Product nesnesinin kendini nasıl kurtaracağını bilmesi gerektiğini söylüyor.

Öyle değil. Nesneler kendi veri yapılarını kapsamaktadır. Bir Ürünün bellek temsilindeki ürün davranışlarını (ne olursa olsun) sergilemekten siz sorumlusunuz; ancak kalıcı depolama oradadır (deponun arkasında) ve kendi işi vardır.

Veritabanının bellek içi gösterimi ile kalıcı belleği arasında veri kopyalamanın bir yolu olmalıdır . Sınırda , işler çok primative almak eğilimindedir.

Temel olarak, yalnızca yazma veritabanları özellikle yararlı değildir ve bellek eşdeğerlerindeki "kalıcı" sıralamadan daha yararlı değildir. ProductEğer bu bilgileri asla çıkarmayacaksanız, bir nesneye bilgi koymanın bir anlamı yoktur . Mutlaka "alıcılar" kullanmazsınız - ürün veri yapısını paylaşmaya çalışmıyorsunuz ve kesinlikle Ürünün dahili temsiline değiştirilebilir erişimi paylaşmamalısınız.

Belki de tasarrufu başka bir nesneye devredebiliriz:

Bu kesinlikle işe yarar - kalıcı depolama alanınız etkili bir şekilde geri arama olur. Muhtemelen arayüzü kolaylaştırır:

interface ProductStorage {
    onProduct(String name, double price);
}

Orada gidiş bilgiler buradan (ve tekrar) orada almak gerektiğinden, hafıza temsil ve depolama mekanizmasında arasında kenetleme edilecek. Paylaşılacak bilgilerin değiştirilmesi görüşmenin her iki ucunu da etkileyecektir. Bu yüzden, bunu yapabildiğimiz her şeyi açık yapabiliriz.

Bu yaklaşım - verilerin geri aramalar yoluyla iletilmesi, TDD'deki alayların gelişiminde önemli bir rol oynamıştır .

Bilgileri geri aramaya iletmenin, bir sorgudan bilgi döndürmeyle aynı kısıtlamalara sahip olduğunu unutmayın - veri yapılarınızın değiştirilebilir kopyalarının etrafından geçmemelisiniz.

Bu yaklaşım, Evans'ın Mavi Kitapta tarif ettiği şeye biraz zıttır; burada bir sorgu yoluyla veri döndürmek, işler hakkında gitmenin normal yoludur ve etki alanı nesneleri, "kalıcılık endişeleri" nde karışmaktan kaçınmak için özel olarak tasarlanmıştır.

DDD'yi bir OOP tekniği olarak anlıyorum ve bu yüzden bu çelişkiyi tam olarak anlamak istiyorum.

Akılda tutulması gereken bir şey - Mavi Kitap on beş yıl önce, Java 1.4 dünyayı dolaşırken yazılmıştır. Özellikle, kitap Java jeneriklerinden önce gelir - şimdi Evans'ın fikirlerini geliştirirken bizim için çok daha fazla tekniğimiz var.


2
Ayrıca bahsetmeye değer: "kendini kaydet" her zaman diğer nesnelerle (bir dosya sistemi nesnesi veya bir veritabanı veya uzak bir web hizmeti) etkileşim gerektirir, bunlardan bazıları erişim kontrolü için bir oturum oluşturulmasını gerektirebilir. Dolayısıyla böyle bir nesne kendi başına ayakta ve bağımsız olmaz. Bu nedenle OOP bunu gerektiremez, çünkü amacı nesneyi kapsüllemek ve kuplajı azaltmaktır.
Christophe

Harika bir cevap için teşekkürler. İlk olarak, Storagearayüzü yaptığınız gibi tasarladım , sonra yüksek kuplajı düşündüm ve değiştirdim. Ama haklısın, yine de kaçınılmaz bir bağlantı var, neden daha açık hale getirmiyoruz?
ttulka

1
"Bu yaklaşım, Evans'ın Mavi Kitapta tarif ettiklerine biraz zıttır" - sonuçta biraz gerginlik var :-) Bu aslında benim sorum, DDD'yi bir OOP tekniği olarak anlıyorum ve bu yüzden bu çelişkinin tamamen anlaşılması.
ttulka

1
Benim tecrübelerime göre, bu şeylerin her biri (genel olarak OOP, DDD, TDD, kısaltmayı seç) kendilerinde iyi ve güzel görünüyor, ancak "gerçek dünya" uygulaması söz konusu olduğunda, her zaman bir miktar ödünleşim veya çalışmak için olması gereken idealizmden daha az.
jleach

Ben sebat (ve sunum) bir şekilde "özel" olduğu fikrine katılmıyorum. Onlar değil. İhtiyaç talebini karşılamak için modellemenin bir parçası olmalıdırlar. Aksine gerçek gereklilikler olmadıkça, uygulama içinde yapay (veri tabanlı) bir sınır olması gerekmez .
Robert Bräutigam

1

Çok iyi gözlemler, onlarla tamamen aynı fikirdeyim. İşte tam olarak bu konuda bir konuşma (sadece düzeltme: slaytlar): Nesneye Dayalı Etki Alanına Dayalı Tasarım .

Kısa cevap: hayır. Başvurunuzda tamamen teknik olan ve alanla alakası olmayan bir nesne olmamalıdır . Bu, kayıt çerçevesini bir muhasebe uygulamasında uygulamak gibidir.

Kişisel Storagearayüz örneği varsayarak mükemmel bir tanesidir Storagebunu yazmak bile sonra bazı dış çerçeve olarak kabul edilir.

Ayrıca, save()bir nesnede yalnızca bu etki alanının bir parçasıysa ("dil") izin verilmelidir. Örneğin, bir aradıktan Accountsonra açıkça "kaydetmek" için gerekli olmamalıdır transfer(amount). Haklı olarak işletme fonksiyonunun transfer()transferimi devam ettirmesini beklemeliyim .

Sonuç olarak, DDD'nin fikirlerinin iyi olduğunu düşünüyorum. Her yerde kullanılan dili kullanmak, etki alanını konuşma, sınırlı bağlamlar vb. İle kullanmak. Ancak yapı taşlarının nesne yönelimi ile uyumlu olması durumunda ciddi bir revizyona ihtiyacı vardır. Ayrıntılar için bağlantılı güverteye bakın.


Konuşmanız izlemek için bir yer mi? (Sadece bağlantının altındaki slaytlar görüyorum). Teşekkürler!
ttulka

Konuşmanın sadece Almanca bir kaydı var, burada: javadevguy.wordpress.com/2018/11/26/…
Robert Bräutigam

Harika konuşma! (Neyse ki Almanca konuşuyorum). Bence tüm blogunuz okumaya değer ... Çalışmanız için teşekkürler!
19:47

Çok anlayışlı bir kaydırıcı Robert. Çok açıklayıcı buldum ama sonunda, kapsülleme ve LoD'yi kırmamaya yönelik çözümlerin çoğunun etki alanı nesnesine çok fazla sorumluluk vermeye dayalı olduğu hissini aldım: yazdırma, serileştirme, UI biçimlendirme, vb. t Alan ve teknik arasındaki bağlantıyı artıran (uygulama ayrıntıları)? Örneğin, AccountNumber, Apache Wicket API'sı ile birleşti. Veya Json nesnesi ne olursa olsun Hesap? Sence bu değer olmaya değer bir bağlantı mı?
Laiv

@Laiv Sorunuzun dilbilgisi, iş işlevlerini uygulamak için teknolojiyi kullanmayla ilgili bir sorun olduğunu gösteriyor? Şöyle söyleyelim: Sorun, etki alanı ve teknoloji arasındaki bağlantı değil, farklı soyutlama düzeyleri arasındaki bağlantı. Örneğin AccountNumber gerektiğini bir şekilde temsil edilebilir olduğunu biliyoruz TextField. Diğerleri ("Görünüm" gibi) bunu bilseydi, bu olmaması gereken bir bağlantıdır, çünkü bu bileşenin neyin AccountNumber, yani içsellerden oluştuğunu bilmesi gerekir .
Robert Bräutigam

1

Belki de tasarrufu başka bir nesneye devredebiliriz

Alanların bilgisini gereksiz yere yaymaktan kaçının. Bir alanı tanımak ne kadar çok şey yaparsa alan eklemek veya kaldırmak o kadar zorlaşır:

public class Product {
    private String name;
    private Double price;

    void save(Storage storage) {
        storage.save( toString() );
    }
}

Burada, bir günlük dosyasına, veritabanına veya her ikisine birden kaydediyorsanız ürünün hiçbir fikri yoktur. Burada 4 veya 40 alanınız varsa save yönteminin hiçbir fikri yoktur. Bu gevşek bir şekilde birleşti. Bu iyi birşey.

Elbette bu, bu hedefe nasıl ulaşacağınıza sadece bir örnektir. DTO'nuz olarak kullanmak için bir dize oluşturmayı ve ayrıştırmayı sevmiyorsanız, bir koleksiyon da kullanabilirsiniz. LinkedHashMapdüzeni koruduğu ve bir günlük dosyasında toString () ifadesinin iyi göründüğü için eski bir favorim.

Bunu nasıl yaparsanız yapın, lütfen etraftaki alanların bilgisini yaymayın. Bu, insanların geç olana kadar genellikle görmezden geldiği bir bağlantı şeklidir. Nesnemin mümkün olduğunca kaç alana sahip olduğunu statik olarak bilmek için birkaç şey istiyorum. Bu şekilde bir alan eklemek pek çok yerde pek çok düzenleme gerektirmez.


Bu aslında soruma gönderdiğim kod, değil mi? A kullandım Map, sen a Stringya da a önerirsin List. Ancak, cevabında @VoiceOfUnreason'ın belirttiği gibi, bağlantı hala açık, sadece açık değil. En azından bir nesne olarak okunduğunda, ürünün veri yapısını hem veritabanına hem de günlük dosyasına kaydetmek için hala gereksizdir.
ttulka

Kaydetme yöntemini değiştirdim ama aksi halde evet aynı. Fark, kaplinin artık statik olmaması ve depolama sistemine bir kod değişikliğini zorlamadan yeni alanların eklenmesine izin vermesidir. Bu, depolama sistemini birçok farklı üründe yeniden kullanılabilir hale getirir. Sadece bir çifte bir ip ve tekrar bir çifte dönüşmek gibi biraz doğal olmayan şeyler yapmaya zorlar. Ama eğer gerçekten bir sorun varsa bu da çözülebilir.
candied_orange


Ama dediğim gibi, hala orada (ayrışarak) kaplini görüyorum, sadece statik (açık) olmamak dezavantajı bir derleyici tarafından kontrol edilemediğini ve daha fazla hataya açık hale getirdiğini görüyorum. Bu Storage, etki alanının bir parçasıdır (depo arabiriminin yanı sıra) ve böyle bir kalıcılık API'sı yapar. Değiştirildiğinde, müşterileri derleme zamanında bilgilendirmek daha iyidir, çünkü çalışma zamanında kırılmamak için yine de tepki vermeleri gerekir.
ttulka

Bu bir yanlış anlama. Derleyici bir günlük dosyasını veya DB'yi denetleyemiyor. Tek kontrolü, bir kod dosyasının günlük dosyası veya DB ile tutarlı olması garanti edilmeyen başka bir kod dosyasıyla tutarlı olup olmadığıdır.
candied_orange

0

Daha önce bahsedilen kalıplara bir alternatif var. Memento kalıbı, bir etki alanı nesnesinin dahili durumunu kapsüllemek için mükemmeldir. Hatıra nesnesi, etki alanı nesnesi genel durumunun bir anlık görüntüsünü temsil eder. Etki alanı nesnesi, bu genel durumun dahili durumundan nasıl oluşturulacağını bilir veya tersi de geçerlidir. Bir depo daha sonra yalnızca devletin kamu temsiliyle çalışır. Bununla birlikte, iç uygulama herhangi bir kalıcılık özelliğinden ayrılmıştır ve sadece kamu sözleşmesini sürdürmek zorundadır. Ayrıca, etki alanı nesneniz gerçekten biraz anemik hale getirecek alıcıları ortaya çıkarmak zorunda değildir.

Bu konu hakkında daha fazla bilgi için, Scott Millett ve Nick Tune tarafından yazılan "Etki Alanına Dayalı Tasarımın Kalıpları, İlkeleri ve Uygulamaları" adlı harika kitabı öneriyorum.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.