Model Domain Kaya - bagaimana tepatnya, apakah perilaku cocok?

84

Dalam perdebatan model domain Rich vs Anemic, internet penuh dengan saran filosofis tetapi pendek pada contoh-contoh otoritatif. Tujuan dari pertanyaan ini adalah untuk menemukan pedoman definitif dan contoh nyata dari model Desain Berbasis Domain yang tepat. (Idealnya dalam C #.)

Sebagai contoh dunia nyata, implementasi DDD ini tampaknya salah:

Model domain WorkItem di bawah tidak lain adalah tas properti, yang digunakan oleh Entity Framework untuk database kode-pertama. Per Fowler, itu adalah anemia .

Lapisan WorkItemService tampaknya merupakan persepsi yang salah tentang Layanan Domain; ini berisi semua logika perilaku / bisnis untuk WorkItem. Per Yemelyanov dan lainnya, ini bersifat prosedural . (hal 6)

Jadi jika di bawah ini salah, bagaimana saya bisa memperbaikinya?
Perilaku, yaitu AddStatusUpdate atau Checkout , harus termasuk dalam kelas WorkItem yang benar?
Ketergantungan apa yang harus dimiliki oleh model WorkItem?

masukkan deskripsi gambar di sini

public class WorkItemService : IWorkItemService {
    private IUnitOfWorkFactory _unitOfWorkFactory;

    //using Unity for dependency injection
    public WorkItemService(IUnitOfWorkFactory unitOfWorkFactory) {
        _unitOfWorkFactory = unitOfWorkFactory;
    }

    public void AddStatusUpdate(int workItemId, int statusId) {

        using (var unitOfWork = _unitOfWorkFactory.GetUnitOfWork<IWorkItemUnitOfWork>()) {
            var workItemRepo = unitOfWork.WorkItemRepository;
            var workItemStatusRepo = unitOfWork.WorkItemStatusRepository;

            var workItem = workItemRepo.Read(wi => wi.Id == workItemId).FirstOrDefault();
            if (workItem == null)
                throw new ArgumentException(string.Format(@"The provided WorkItem Id '{0}' is not recognized", workItemId), "workItemId");

            var status = workItemStatusRepo.Read(s => s.Id == statusId).FirstOrDefault();
            if (status == null)
                throw new ArgumentException(string.Format(@"The provided Status Id '{0}' is not recognized", statusId), "statusId");

            workItem.StatusHistory.Add(status);

            workItemRepo.Update(workItem);
            unitOfWork.Save();
        }
    }
}

(Contoh ini disederhanakan agar lebih mudah dibaca. Kode ini jelas masih kikuk, karena ini merupakan upaya yang membingungkan, tetapi perilaku domainnya adalah: perbarui status dengan menambahkan status baru ke riwayat arsip. Pada akhirnya saya setuju dengan jawaban lain, ini hanya bisa ditangani oleh CRUD.)

Memperbarui

@AlexeyZimarev memberikan jawaban terbaik, video yang sempurna tentang subjek dalam C # oleh Jimmy Bogard, tetapi tampaknya dipindahkan ke komentar di bawah karena tidak memberikan informasi yang cukup di luar tautan. Saya memiliki konsep kasar catatan saya yang merangkum video dalam jawaban saya di bawah ini. Silakan mengomentari jawaban dengan koreksi apa pun. Video ini berdurasi satu jam tetapi sangat layak ditonton.

Perbarui - 2 Tahun Kemudian

Saya pikir itu adalah tanda kedewasaan DDD yang baru lahir bahwa bahkan setelah mempelajarinya selama 2 tahun, saya masih tidak bisa berjanji bahwa saya tahu "cara yang benar" untuk melakukannya. Bahasa di mana-mana, akar agregat, dan pendekatannya terhadap desain yang didorong perilaku adalah kontribusi berharga DDD untuk industri. Ketidaktahuan yang gigih dan sumber acara menyebabkan kebingungan, dan saya pikir filsafat seperti itu menahannya dari adopsi yang lebih luas. Tetapi jika saya harus melakukan kode ini lagi, dengan apa yang telah saya pelajari, saya pikir akan terlihat seperti ini:

masukkan deskripsi gambar di sini

Saya masih menyambut jawaban apa pun untuk pos ini (sangat aktif) yang memberikan kode praktik terbaik untuk model domain yang valid.

RJB
sumber
6
Semua teori filsafat jatuh ke tanah ketika Anda memberi tahu mereka "I don't want to duplicate all my entities into DTOs simply because I don't need it and it violates DRY, and I also don't want my client application to take a dependency on EntityFramework.dll". "Entitas" dalam jargon Entity Framework tidak sama dengan "Entities" seperti pada "Domain Model"
Federico Berasategui
Saya baik-baik saja dengan menduplikasi entitas domain saya menjadi DTO, menggunakan alat otomatis seperti Automapper, jika itu yang diperlukan. Saya hanya tidak yakin bagaimana itu seharusnya terlihat pada akhir hari.
RJB
16
Saya akan merekomendasikan Anda untuk menonton sesi NDC 2012 Jimmy Bogard "Crafting Wicked Domain Models" di Vimeo . Dia menjelaskan apa seharusnya domain kaya dan bagaimana menerapkannya dalam kehidupan nyata dengan memiliki perilaku di entitas Anda. Contohnya sangat praktis dan semuanya dalam C #.
Alexey Zimarev
Terima kasih, saya setengah jalan melalui video dan sejauh ini sempurna. Saya tahu bahwa jika ini salah, pasti ada jawaban "benar" di suatu tempat ....
RJB
2
Saya juga menuntut cinta untuk Java: /
uylmz

Jawaban:

59

Jawaban yang paling membantu diberikan oleh Alexey Zimarev dan mendapatkan setidaknya 7 upvotes sebelum moderator memindahkannya ke komentar di bawah pertanyaan awal saya ....

Jawabannya:

Saya akan merekomendasikan Anda untuk menonton sesi NDC 2012 Jimmy Bogard "Crafting Wicked Domain Models" di Vimeo. Dia menjelaskan apa seharusnya domain kaya dan bagaimana menerapkannya dalam kehidupan nyata dengan memiliki perilaku di entitas Anda. Contohnya sangat praktis dan semuanya dalam C #.

http://vimeo.com/43598193

Saya membuat beberapa catatan untuk merangkum video tersebut untuk kepentingan tim saya dan untuk memberikan sedikit detail lebih cepat dalam posting ini. (Video ini berdurasi satu jam, tetapi benar-benar bernilai setiap menit jika Anda punya waktu. Jimmy Bogard layak mendapatkan banyak pujian untuk penjelasannya.)

  • "Untuk sebagian besar aplikasi ... kita tidak tahu bahwa itu akan menjadi kompleks ketika kita mulai. Mereka hanya menjadi seperti itu."
    • Kompleksitas tumbuh secara alami ketika kode dan persyaratan ditambahkan. Aplikasi dapat dimulai dengan sangat sederhana, seperti CRUD, tetapi perilaku / aturan dapat dipanggang.
    • "Yang menyenangkan adalah kita tidak harus memulai dari yang rumit. Kita bisa mulai dengan model domain anemia, itu hanya tas properti, dan hanya dengan teknik refactoring standar kita bisa bergerak menuju model domain yang benar."
  • Model domain = objek bisnis. Perilaku domain = aturan bisnis.
  • Perilaku sering disembunyikan dalam aplikasi - itu bisa di PageLoad, Button1_Click, atau sering di kelas pembantu seperti 'FooManager' atau 'FooService'.
  • Aturan bisnis yang terpisah dari objek domain "mengharuskan kita untuk mengingat" aturan itu.
    • Dalam contoh pribadi saya di atas, satu aturan bisnis adalah WorkItem.StatusHistory.Add (). Kami tidak hanya mengubah status, kami mengarsipkannya untuk audit.
  • Perilaku domain "menghilangkan bug dalam aplikasi jauh lebih mudah daripada hanya menulis banyak tes." Tes mengharuskan Anda tahu untuk menulis tes tersebut. Perilaku domain menawarkan Anda jalur yang benar untuk diuji .
  • Layanan domain adalah "kelas pembantu untuk mengoordinasikan kegiatan antara entitas model domain yang berbeda."
    • Layanan domain! = Perilaku domain. Entitas memiliki perilaku, layanan domain hanyalah perantara antara entitas.
  • Objek domain seharusnya tidak memiliki infrastruktur yang mereka butuhkan (yaitu IOfferCalculatorService). Layanan infrastruktur harus diteruskan ke model domain yang menggunakannya.
  • Model domain harus menawarkan untuk memberi tahu Anda apa yang dapat mereka lakukan, dan mereka hanya dapat melakukan hal-hal itu.
  • Properti model domain harus dijaga dengan setter pribadi, sehingga hanya model yang dapat mengatur propertinya sendiri, melalui perilakunya sendiri . Kalau tidak, itu "promiscuous."
  • Objek model domain anemia, yang hanya tas properti untuk ORM, hanya "lapisan tipis - versi yang sangat diketik atas database."
    • "Betapapun mudahnya untuk mendapatkan baris database ke objek, itulah yang kami punya."
    • 'Model objek yang paling tahan hanya itu. Apa yang membedakan model domain anemik versus aplikasi yang tidak benar-benar memiliki perilaku, adalah jika suatu objek memiliki aturan bisnis, tetapi aturan itu tidak ditemukan dalam model domain. '
  • "Untuk banyak aplikasi, tidak ada kebutuhan nyata untuk membangun segala jenis lapisan logika aplikasi bisnis nyata, itu hanya sesuatu yang dapat berbicara dengan database dan mungkin beberapa cara mudah untuk mewakili data yang ada di sana."
    • Jadi dengan kata lain, jika semua yang Anda lakukan adalah CRUD tanpa objek bisnis atau aturan perilaku khusus, Anda tidak perlu DDD.

Jangan ragu untuk berkomentar dengan poin lain yang menurut Anda harus dimasukkan, atau jika menurut Anda salah satu dari catatan ini tidak tepat sasaran. Mencoba mengutip secara langsung atau memparafrasekan sebanyak mungkin.

RJB
sumber
Video hebat terutama untuk melihat bagaimana refactoring bekerja dalam suatu alat. Sebagian besar tentang enkapsulasi objek domain yang tepat (untuk memastikan mereka konsisten). Dia melakukan pekerjaan yang bagus dengan memberi tahu aturan bisnis tentang penawaran, anggota, dll. Dia menyebutkan kata invarian beberapa kali (yang merupakan pemodelan domain berbasis kontrak). Saya berharap kode .net akan berkomunikasi dengan lebih baik apa itu aturan bisnis formal, karena perubahan itu dan Anda harus memeliharanya.
Fuhrmanator
6

Pertanyaan Anda tidak dapat dijawab, karena contoh Anda salah. Khususnya, karena tidak ada perilaku. Paling tidak di area domain Anda. Contoh AddStatusUpdatemetode bukan logika domain, tetapi logika yang menggunakan domain itu. Logika semacam itu masuk akal untuk berada di dalam semacam layanan, yang menangani permintaan dari luar.

Misalnya, jika ada persyaratan bahwa item kerja tertentu hanya dapat memiliki status tertentu, atau hanya dapat memiliki status N, maka itu adalah logika domain dan harus menjadi bagian dari salah satu WorkItematau StatusHistorysebagai metode.

Alasan kebingungan Anda adalah karena Anda mencoba menerapkan pedoman untuk kode yang tidak memerlukannya. Model domain hanya relevan jika Anda memiliki banyak logika domain yang kompleks. Misalnya. logika yang bekerja pada entitas itu sendiri dan berasal dari persyaratan. Jika kode adalah tentang memanipulasi entitas dari data luar, maka itu kemungkinan besar bukan logika domain. Tetapi saat Anda mendapatkan banyak ifberdasarkan pada data dan entitas yang Anda kerjakan, maka itu adalah logika domain.

Salah satu masalah pemodelan domain sejati adalah bahwa hal ini adalah tentang mengelola persyaratan yang kompleks. Dan dengan demikian kekuatan dan manfaatnya yang sesungguhnya tidak dapat ditampilkan dengan kode sederhana. Anda memerlukan banyak entitas dengan banyak persyaratan di sekitar mereka untuk benar-benar melihat manfaatnya. Sekali lagi, contoh Anda terlalu sederhana untuk model domain untuk benar-benar bersinar.

Akhirnya, beberapa hal OT yang akan saya sebutkan adalah bahwa model domain yang benar dengan desain OOP nyata akan sangat sulit untuk bertahan menggunakan Entity Framework. Sementara ORM dirancang dengan memetakan struktur OOP yang benar ke yang relasional, masih ada banyak masalah, dan model relasional akan sering bocor ke dalam model OOP. Bahkan dengan nHibernate, yang saya anggap jauh lebih kuat daripada EF, ini bisa menjadi masalah.

Euforia
sumber
Poin bagus. Di mana metode AddStatusUpdate berada saat itu, di Data atau proyek lain di Infrastrcture? Apa contoh perilaku yang secara teoritis termasuk dalam WorkItem? Setiap kode psuedo atau mock-up akan sangat dihargai. Contoh saya sebenarnya disederhanakan agar lebih mudah dibaca. Ada entitas lain, dan misalnya AddStatusUpdate memiliki beberapa perilaku ekstra - itu benar-benar mengambil nama kategori status, dan jika kategori itu tidak ada, kategori dibuat.
RJB
@RJB Seperti yang saya katakan, AddStatusUpdate adalah kode yang menggunakan domain. Jadi entah itu semacam layanan web atau aplikasi yang menggunakan kelas domain. Dan seperti yang saya katakan, Anda tidak dapat mengharapkan segala jenis mockup atau pseudocode, karena Anda perlu membuat keseluruhan proyek dengan kompleksitas yang cukup besar untuk menunjukkan keunggulan nyata dari model domain OOP.
Euforia
5

Asumsi Anda bahwa merangkum logika bisnis Anda yang terkait dengan WorkItem ke dalam "layanan gemuk" adalah anti-pola yang melekat yang saya berpendapat tidak harus.

Terlepas dari pemikiran Anda tentang model domain anemia, pola dan praktik standar yang khas dari aplikasi Lini Bisnis. NET mendorong pendekatan berlapis transaksional yang terdiri dari berbagai komponen. Mereka mendorong pemisahan logika bisnis dari model domain secara khusus untuk memfasilitasi komunikasi dari model domain umum di komponen .NET lainnya serta komponen pada tumpukan teknologi yang berbeda atau lintas tingkatan fisik.

Salah satu contohnya adalah layanan web SOAP berbasis NET yang berkomunikasi dengan aplikasi klien Silverlight yang kebetulan memiliki DLL yang berisi tipe data sederhana. Proyek entitas domain ini dapat dibangun menjadi rakitan .NET atau rakitan Silverlight, di mana komponen Silverlight yang tertarik yang memiliki DLL ini tidak akan terkena perilaku objek yang mungkin bergantung pada komponen yang hanya tersedia untuk layanan.

Terlepas dari sikap Anda pada debat ini, ini adalah pola yang diadopsi dan diterima yang diajukan oleh Microsoft dan menurut pendapat profesional saya ini bukan pendekatan yang salah, tetapi kemudian model objek yang mendefinisikan perilakunya sendiri tidak harus merupakan anti-pola juga. Jika Anda melangkah maju dengan desain ini, yang terbaik adalah menyadari dan memahami beberapa batasan dan titik sakit yang mungkin Anda temui jika Anda perlu berintegrasi dengan komponen lain yang perlu melihat model domain Anda. Dalam kasus tertentu mungkin Anda mungkin ingin memiliki Penerjemah mengubah model domain gaya berorientasi objek Anda menjadi objek data sederhana yang tidak memaparkan metode perilaku tertentu.

maple_shaft
sumber
1
1) Bagaimana Anda bisa memisahkan logika bisnis dari model domain? Ini adalah domain tempat logika bisnis ini hidup; entitas dalam domain tersebut menjalankan perilaku yang terkait dengan logika bisnis itu. Dunia nyata tidak memiliki layanan, juga tidak ada di kepala para ahli domain. 2) Setiap komponen yang ingin diintegrasikan dengan Anda perlu membangun model domain sendiri, karena kebutuhannya akan berbeda dan akan memiliki pandangan yang berbeda pada model domain Anda. Ini adalah kekuatan lama yang bisa Anda buat satu model domain yang bisa dibagikan.
Stefan Billiet
1
@StefanBilliet Itu adalah poin bagus tentang kekeliruan model domain universal, tetapi dimungkinkan dalam komponen yang lebih sederhana dan interaksi komponen seperti yang telah saya lakukan sebelumnya. Pendapat saya adalah bahwa logika penerjemahan antara model domain dapat membuat banyak kode yang membosankan dan tidak jelas dan jika dapat dihindari dengan aman maka itu bisa menjadi pilihan desain yang baik.
maple_shaft
1
Sejujurnya, saya pikir satu-satunya pilihan desain yang bagus adalah model yang dapat dipertimbangkan oleh pakar bisnis. Anda sedang membangun model domain, untuk digunakan bisnis untuk menyelesaikan masalah tertentu dalam domain itu. Memisahkan perilaku dari entitas domain menjadi layanan menjadikannya lebih sulit bagi semua orang yang terlibat, karena Anda harus terus memetakan apa yang dikatakan pakar domain ke kode layanan yang hampir tidak memiliki kemiripan dengan percakapan saat ini. Dalam pengalaman saya, Anda kehilangan lebih banyak waktu dengan itu, daripada mengetik boilerplate. Itu tidak berarti tidak ada cara di sekitar kode tempat boiler.
Stefan Billiet
@StefanBilliet Di dunia yang sempurna saya setuju dengan Anda di mana pakar bisnis memiliki waktu untuk duduk bersama pengembang. Kenyataan dari industri perangkat lunak adalah bahwa pakar bisnis tidak memiliki waktu atau minat untuk terlibat pada tingkat ini atau lebih buruk lagi, namun para pengembang diharapkan untuk mengetahuinya hanya dengan panduan yang tidak jelas.
maple_shaft
Benar, tapi itu bukan alasan untuk menerima kenyataan itu. Untuk melanjutkan pengejaran seperti itu berarti membuang waktu (dan mungkin reputasi) dari pengembang dan uang pelanggan. Proses yang saya jelaskan adalah hubungan yang perlu dibangun dari waktu ke waktu; itu membutuhkan banyak usaha, tetapi menghasilkan hasil yang jauh lebih baik. Ada alasan bahwa "Bahasa yang Tidak Berarti" sering dianggap sebagai aspek terpenting dari DDD.
Stefan Billiet
5

Saya menyadari pertanyaan ini sudah cukup tua sehingga jawaban ini untuk anak cucu. Saya ingin menjawab dengan contoh konkret alih-alih berdasarkan pada teori.

Meringkas "perubahan status item pekerjaan" di WorkItemkelas seperti:

public SomeStatusUpdateType Status { get; private set; }

public void ChangeStatus(SomeStatusUpdateType status)
{
    // Maybe we designed this badly at first ;-)
    Status = status;       
}

Sekarang WorkItemkelas Anda bertanggung jawab untuk mempertahankan diri dalam keadaan hukum. Namun implementasinya sangat lemah. Pemilik produk menginginkan riwayat semua pembaruan status yang dibuat untuk WorkItem.

Kami mengubahnya menjadi seperti ini:

private ICollection<SomeStatusUpdateType> StatusUpdates { get; private set; }
public SomeStatusUpdateType Status => StatusUpdates.OrderByDescending(s => s.CreatedOn).FirstOrDefault();

public void ChangeStatus(SomeStatusUpdateType status)
{
    // Better...
    StatusUpdates.Add(status);       
}

Implementasi telah berubah secara drastis tetapi penelepon ChangeStatusmetode ini tidak mengetahui rincian implementasi yang mendasarinya dan tidak memiliki alasan untuk mengubahnya sendiri.

Ini adalah contoh entitas model domain yang kaya, IMHO.

Mengenakan
sumber