Mô hình miền phong phú - chính xác, hành vi phù hợp như thế nào?


84

Trong cuộc tranh luận về các mô hình miền Rich vs Anemia, internet chứa đầy những lời khuyên triết học nhưng ngắn gọn về các ví dụ có thẩm quyền. Mục tiêu của câu hỏi này là tìm ra các hướng dẫn dứt khoát và các ví dụ cụ thể về các mô hình Thiết kế hướng miền thích hợp. (Lý tưởng nhất là trong C #.)

Đối với một ví dụ thực tế, việc triển khai DDD này dường như là sai:

Các mô hình miền WorkItem bên dưới không có gì ngoài các túi thuộc tính, được Entity Framework sử dụng cho cơ sở dữ liệu mã đầu tiên. Theo Fowler, nó là thiếu máu .

Lớp WorkItemService rõ ràng là một hiểu lầm phổ biến về Dịch vụ miền; nó chứa tất cả logic hành vi / nghiệp vụ cho WorkItem. Per Y Extremeanov và những người khác, nó là thủ tục . (trang 6)

Vì vậy, nếu dưới đây là sai, làm thế nào tôi có thể làm cho nó đúng?
Hành vi, tức là AddStatusUpdate hoặc Checkout , có nên thuộc lớp WorkItem đúng không?
Mô hình WorkItem nên có những phụ thuộc nào?

nhập mô tả hình ảnh ở đây

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();
        }
    }
}

. chỉ có thể được xử lý bởi CRUD.)

Cập nhật

@AlexeyZimarev đã đưa ra câu trả lời hay nhất, một video hoàn hảo về chủ đề trong C # của Jimmy Bogard, nhưng rõ ràng nó đã được chuyển vào một bình luận bên dưới vì nó không cung cấp đủ thông tin ngoài liên kết. Tôi có một bản thảo sơ bộ các ghi chú của tôi tóm tắt video trong câu trả lời của tôi dưới đây. Xin vui lòng bình luận về câu trả lời với bất kỳ sửa chữa. Video dài một giờ nhưng rất đáng xem.

Cập nhật - 2 năm sau

Tôi nghĩ đó là dấu hiệu của sự trưởng thành non trẻ của DDD mà ngay cả sau khi nghiên cứu nó được 2 năm, tôi vẫn không thể hứa rằng mình biết "cách làm đúng". Ngôn ngữ phổ biến, nguồn gốc tổng hợp và cách tiếp cận của nó đối với thiết kế hướng hành vi là những đóng góp có giá trị của DDD cho ngành công nghiệp. Sự thiếu hiểu biết dai dẳng và tìm nguồn cung ứng sự kiện gây ra sự nhầm lẫn và tôi nghĩ triết lý như thế giữ nó lại từ việc áp dụng rộng rãi hơn. Nhưng nếu tôi phải thực hiện lại mã này, với những gì tôi đã học được, tôi nghĩ nó sẽ trông giống như thế này:

nhập mô tả hình ảnh ở đây

Tôi vẫn hoan nghênh bất kỳ câu trả lời nào cho bài đăng (rất tích cực) này cung cấp bất kỳ mã thực hành tốt nhất nào cho một mô hình miền hợp lệ.


6
Tất cả các lý thuyết triết học rơi ngay xuống đất khi bạn nói với họ "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". "Các thực thể" trong biệt ngữ Khung thực thể không giống như "Các thực thể" như trong "Mô hình miền"
Federico Berasargetui

Tôi ổn với việc sao chép các thực thể miền của mình thành DTO, sử dụng một công cụ tự động như Automapper, nếu đó là những gì nó cần. Tôi chỉ không chắc là nó sẽ trông như thế nào vào cuối ngày.
RJB

16
Tôi sẽ khuyên bạn nên xem phiên NDC 2012 của Jimmy Bogard "Chế tạo mô hình miền xấu" trên Vimeo . Ông giải thích tên miền phong phú nên là gì và cách triển khai chúng trong cuộc sống thực bằng cách có hành vi trong các thực thể của bạn. Ví dụ rất thực tế và tất cả trong C #.
Alexey Zimarev

Cảm ơn bạn, tôi đang xem được một nửa video và điều này là hoàn hảo cho đến nay. Tôi biết rằng nếu điều này là sai, thì phải có câu trả lời "đúng" ở đâu đó ....
RJB

2
Tôi cũng yêu thích Java: /
uylmz

Câu trả lời:


59

Câu trả lời hữu ích nhất được đưa ra bởi Alexey Zimarev và nhận được ít nhất 7 lượt upvote trước khi người điều hành chuyển nó thành một bình luận bên dưới câu hỏi ban đầu của tôi ....

Câu trả lời của anh ấy:

Tôi muốn giới thiệu bạn nên xem phiên NDC 2012 "Chế tạo mô hình miền độc ác" của Jimmy Bogard trên Vimeo. Ông giải thích tên miền phong phú nên là gì và cách triển khai chúng trong cuộc sống thực bằng cách có hành vi trong các thực thể của bạn. Ví dụ rất thực tế và tất cả trong C #.

http://vimeo.com/43598193

Tôi đã ghi lại một số lưu ý để tóm tắt video cho cả lợi ích của nhóm tôi và để cung cấp thêm một chút chi tiết ngay lập tức trong bài đăng này. (Video dài một giờ, nhưng thực sự đáng giá từng phút nếu bạn có thời gian. Jimmy Bogard xứng đáng nhận được rất nhiều tín dụng cho lời giải thích của anh ấy.)

  • "Đối với hầu hết các ứng dụng ... chúng tôi không biết rằng chúng sẽ phức tạp khi chúng tôi bắt đầu. Chúng chỉ trở thành như vậy."
    • Sự phức tạp phát triển tự nhiên khi mã và các yêu cầu được thêm vào. Các ứng dụng có thể bắt đầu rất đơn giản, như CRUD, nhưng hành vi / quy tắc có thể trở nên phổ biến.
    • "Điều tốt đẹp là chúng ta không phải bắt đầu phức tạp. Chúng ta có thể bắt đầu với mô hình miền thiếu máu, đó chỉ là túi tài sản và chỉ với các kỹ thuật tái cấu trúc tiêu chuẩn, chúng ta có thể tiến tới một mô hình miền thực sự."
  • Mô hình miền = đối tượng kinh doanh. Hành vi tên miền = quy tắc kinh doanh.
  • Hành vi thường bị ẩn trong một ứng dụng - nó có thể nằm trong PageLoad, Nút1_Click hoặc thường trong các lớp trợ giúp như 'FooManager' hoặc 'FooService'.
  • Các quy tắc kinh doanh tách biệt với các đối tượng miền "yêu cầu chúng tôi ghi nhớ" các quy tắc đó.
    • Trong ví dụ cá nhân của tôi ở trên, một quy tắc kinh doanh là WorkItem.StatusHistory.Add (). Chúng tôi không chỉ thay đổi trạng thái, chúng tôi đang lưu trữ nó để kiểm toán.
  • Các hành vi tên miền "loại bỏ lỗi trong một ứng dụng dễ dàng hơn nhiều so với việc chỉ viết một loạt các bài kiểm tra." Các bài kiểm tra yêu cầu bạn phải biết để viết các bài kiểm tra đó. Các hành vi tên miền cung cấp cho bạn các đường dẫn đúng để kiểm tra .
  • Các dịch vụ miền là "các lớp trợ giúp để phối hợp các hoạt động giữa các thực thể mô hình miền khác nhau."
    • Dịch vụ tên miền! = Hành vi tên miền. Các thực thể có hành vi, dịch vụ miền chỉ là trung gian giữa các thực thể.
  • Các đối tượng miền không nên sở hữu cơ sở hạ tầng mà họ cần (ví dụ I OffererCalculatorService). Dịch vụ cơ sở hạ tầng nên được chuyển đến mô hình miền sử dụng nó.
  • Các mô hình miền sẽ cung cấp cho bạn biết những gì họ có thể làm và họ chỉ có thể làm những việc đó.
  • Các thuộc tính của các mô hình miền cần được bảo vệ bằng các setters riêng, để chỉ mô hình có thể thiết lập các thuộc tính của riêng nó, thông qua các hành vi của chính nó . Nếu không thì "lăng nhăng".
  • Các đối tượng mô hình miền thiếu máu, chỉ là túi thuộc tính cho ORM, chỉ là "veneer mỏng - phiên bản được gõ mạnh trên cơ sở dữ liệu."
    • "Tuy nhiên, thật dễ dàng để có được một hàng cơ sở dữ liệu vào một đối tượng, đó là những gì chúng ta có."
    • 'Hầu hết các mô hình đối tượng liên tục chỉ có vậy. Điều khác biệt giữa mô hình miền thiếu máu so với ứng dụng không thực sự có hành vi là nếu một đối tượng có quy tắc kinh doanh, nhưng những quy tắc đó không được tìm thấy trong mô hình miền. '
  • "Đối với nhiều ứng dụng, không có nhu cầu thực sự để xây dựng bất kỳ loại logic ứng dụng kinh doanh thực tế nào, nó chỉ là thứ có thể nói chuyện với cơ sở dữ liệu và có lẽ là một cách dễ dàng để biểu diễn dữ liệu trong đó."
    • Vì vậy, nói cách khác, nếu tất cả những gì bạn đang làm là CRUD không có đối tượng kinh doanh đặc biệt hoặc quy tắc hành vi, bạn không cần DDD.

Xin vui lòng bình luận với bất kỳ điểm nào khác mà bạn cảm thấy nên được đưa vào, hoặc nếu bạn nghĩ rằng bất kỳ ghi chú nào trong số này là không đúng. Đã thử trích dẫn trực tiếp hoặc diễn giải càng nhiều càng tốt.


Video tuyệt vời đặc biệt để xem cách tái cấu trúc hoạt động trong một công cụ. Phần lớn là về đóng gói đúng các đối tượng miền (để đảm bảo chúng nhất quán). Anh ấy làm một công việc tuyệt vời để nói các quy tắc kinh doanh về cung cấp, các thành viên, vv Anh ấy đề cập đến từ bất biến một vài lần (đó là mô hình miền dựa trên hợp đồng). Tôi muốn mã .net sẽ giao tiếp tốt hơn quy tắc kinh doanh chính thức là gì, vì những thay đổi đó và bạn cần duy trì chúng.
Fuhrmanator

6

Câu hỏi của bạn không thể được trả lời, vì ví dụ của bạn sai. Cụ thể, vì không có hành vi. Ít nhất là không trong khu vực của tên miền của bạn. Ví dụ về AddStatusUpdatephương thức không phải là logic miền, mà là logic sử dụng miền đó. Loại logic đó có nghĩa là bên trong một loại dịch vụ nào đó, xử lý các yêu cầu bên ngoài.

Ví dụ, nếu có yêu cầu rằng một mục công việc cụ thể chỉ có thể có các trạng thái cụ thể hoặc nó chỉ có thể có các trạng thái N, thì đó là logic miền và phải là một phần của một WorkItemhoặc StatusHistorymột phương thức.

Lý do cho sự nhầm lẫn của bạn là vì bạn đang cố gắng áp dụng một hướng dẫn cho mã không cần nó. Các mô hình miền chỉ có liên quan nếu bạn có nhiều logic miền phức tạp. Ví dụ. logic hoạt động trên chính các thực thể và bắt nguồn từ các yêu cầu. Nếu mã là về việc thao túng các thực thể từ dữ liệu bên ngoài, thì đó có lẽ không phải là logic miền. Nhưng thời điểm bạn nhận được rất nhiều ifs dựa trên dữ liệu và thực thể bạn đang làm việc với, thì đó là logic miền.

Một trong những vấn đề của mô hình miền thực sự là về việc quản lý các yêu cầu phức tạp. Và như vậy sức mạnh và lợi ích thực sự của nó không thể được thể hiện trên mã đơn giản. Bạn cần hàng tá thực thể với hàng tấn yêu cầu xung quanh chúng để thực sự thấy được lợi ích. Một lần nữa, ví dụ của bạn quá đơn giản để mô hình miền thực sự tỏa sáng.

Cuối cùng, một số điều OT tôi muốn đề cập là một mô hình miền thực với thiết kế OOP thực sự sẽ rất khó để sử dụng Entity Framework. Mặc dù các ORM được thiết kế với ánh xạ cấu trúc OOP thực tới các cấu trúc quan hệ, vẫn còn nhiều vấn đề và mô hình quan hệ thường sẽ rò rỉ vào mô hình OOP. Ngay cả với nHibernate, mà tôi cho là mạnh hơn nhiều so với EF, đây có thể là một vấn đề.


Điểm tốt. Phương thức AddStatusUpdate sẽ thuộc về đâu, trong Dữ liệu hoặc dự án khác trong Cơ sở hạ tầng? Ví dụ về bất kỳ hành vi nào có thể thuộc về lý thuyết trong WorkItem? Bất kỳ mã psuedo hoặc mock-up sẽ được đánh giá rất cao. Ví dụ của tôi đã thực sự đơn giản hóa để dễ đọc hơn. Có các thực thể khác, và ví dụ, AddStatusUpdate có một số hành vi bổ sung - nó thực sự có một tên danh mục trạng thái và nếu danh mục đó không tồn tại, danh mục sẽ được tạo.
RJB

@RJB Giống như tôi đã nói, AddStatusUpdate là mã đang sử dụng tên miền. Vì vậy, một số loại dịch vụ web hoặc ứng dụng sử dụng các lớp miền. Và như tôi đã nói, bạn không thể mong đợi bất kỳ loại mockup hoặc mã giả nào, bởi vì bạn sẽ cần phải thực hiện toàn bộ dự án đủ phức tạp để thể hiện lợi thế thực sự của mô hình miền OOP.
Euphoric

5

Giả định của bạn rằng gói gọn logic kinh doanh của bạn liên quan đến WorkItem vào một "dịch vụ béo" là một mô hình chống đối cố hữu mà tôi cho rằng không nhất thiết phải như vậy.

Bất kể suy nghĩ của bạn về mô hình miền thiếu máu, các mẫu và thông lệ tiêu chuẩn điển hình của ứng dụng Line of Business .NET khuyến khích cách tiếp cận lớp giao dịch bao gồm nhiều thành phần khác nhau. Họ khuyến khích tách logic kinh doanh khỏi mô hình miền cụ thể để tạo điều kiện giao tiếp cho mô hình miền phổ biến trên các thành phần .NET khác cũng như các thành phần trên các ngăn xếp công nghệ khác nhau hoặc trên các tầng vật lý.

Một ví dụ về điều này sẽ là một dịch vụ web SOAP dựa trên .NET giao tiếp với ứng dụng khách Silverlight tình cờ có một DLL chứa các kiểu dữ liệu đơn giản. Dự án thực thể miền này có thể được xây dựng thành một tổ hợp .NET hoặc một tổ hợp Silverlight, trong đó các thành phần Silverlight quan tâm có DLL này sẽ không tiếp xúc với các hành vi đối tượng có thể phụ thuộc vào các thành phần chỉ có sẵn cho dịch vụ.

Bất kể lập trường của bạn về cuộc tranh luận này, đây là mô hình được chấp nhận và chấp nhận do Microsoft đưa ra và theo ý kiến ​​chuyên môn của tôi, đó không phải là một cách tiếp cận sai nhưng sau đó, một mô hình đối tượng xác định hành vi của chính nó cũng không nhất thiết là một mô hình chống đối. Nếu bạn tiếp tục với thiết kế này, tốt nhất là nhận ra và hiểu một số hạn chế và điểm đau mà bạn có thể gặp phải nếu bạn cần tích hợp với các thành phần khác cần xem mô hình miền của mình. Trong trường hợp cụ thể đó, có lẽ bạn có thể muốn có Trình dịch chuyển đổi mô hình miền kiểu hướng đối tượng của bạn thành các đối tượng dữ liệu đơn giản không phơi bày các phương thức hành vi nhất định.


1
1) Làm thế nào bạn có thể tách logic kinh doanh khỏi mô hình miền? Đó là lĩnh vực mà logic kinh doanh này sống; các thực thể trong miền đó đang thực thi hành vi liên quan đến logic kinh doanh đó. Thế giới thực không có dịch vụ, chúng cũng không tồn tại trong đầu của các chuyên gia tên miền. 2) Bất kỳ thành phần nào muốn tích hợp với bạn đều cần xây dựng mô hình miền riêng, bởi vì nhu cầu của nó sẽ khác nhau và nó sẽ có một cái nhìn khác về mô hình miền của bạn. Đó là một sai lầm lâu dài mà bạn có thể tạo một mô hình miền có thể được chia sẻ xung quanh.
Stefan Billiet

1
@StefanBilliet Đó là những điểm tốt về sự sai lầm của một mô hình miền phổ quát, nhưng có thể trong các thành phần đơn giản hơn và tương tác thành phần như tôi đã làm điều này trước đây. Ý kiến ​​của tôi là logic dịch giữa các mô hình miền có thể tạo ra rất nhiều mã tẻ nhạt và nồi hơi và nếu nó có thể tránh được một cách an toàn thì đó có thể là một lựa chọn thiết kế tốt.
maple_shaft

1
Thành thật mà nói, tôi nghĩ rằng sự lựa chọn thiết kế tốt duy nhất là một mô hình mà một chuyên gia kinh doanh có thể lý do. Bạn đang xây dựng mô hình của một tên miền, cho một doanh nghiệp sử dụng để giải quyết các vấn đề nhất định trong miền đó. Việc tách hành vi từ các thực thể miền thành các dịch vụ khiến mọi người tham gia khó khăn hơn, bởi vì bạn liên tục phải ánh xạ những gì một chuyên gia tên miền nói với mã dịch vụ hầu như không giống với cuộc trò chuyện hiện tại. Theo kinh nghiệm của tôi, bạn mất nhiều thời gian hơn với điều đó, hơn là gõ bản soạn sẵn. Điều đó không có nghĩa là không có cách nào xung quanh mã khóa học lò hơi.
Stefan Billiet

@StefanBilliet Trong một thế giới hoàn hảo, tôi đồng ý với bạn nơi một chuyên gia kinh doanh có thời gian ngồi lại với các nhà phát triển. Thực tế của ngành công nghiệp phần mềm là chuyên gia kinh doanh không có thời gian hay hứng thú với việc tham gia ở cấp độ này hoặc tệ hơn nữa là các nhà phát triển dự kiến ​​sẽ chỉ tìm ra nó chỉ với hướng dẫn mơ hồ.
maple_shaft

Đúng, nhưng đó không phải là lý do để chấp nhận thực tế đó. Tiếp tục theo đuổi như vậy là lãng phí thời gian (và có thể là danh tiếng) của các nhà phát triển và tiền của khách hàng. Các thủ tục tôi mô tả là một mối quan hệ cần được xây dựng theo thời gian; nó đòi hỏi rất nhiều nỗ lực, nhưng nó mang lại kết quả tốt hơn nhiều. Có một lý do mà "Ngôn ngữ phổ biến" thường được coi là khía cạnh quan trọng nhất của DDD.
Stefan Billiet

5

Tôi nhận ra câu hỏi này khá cũ nên câu trả lời này là dành cho hậu thế. Tôi muốn trả lời bằng một ví dụ cụ thể thay vì dựa trên lý thuyết.

Đóng gói "thay đổi trạng thái mục công việc" trên WorkItemlớp như vậy:

public SomeStatusUpdateType Status { get; private set; }

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

Bây giờ WorkItemlớp học của bạn có trách nhiệm duy trì chính nó trong một trạng thái pháp lý. Việc thực hiện là khá yếu, tuy nhiên. Chủ sở hữu sản phẩm muốn có một lịch sử của tất cả các cập nhật trạng thái được thực hiện cho WorkItem.

Chúng tôi thay đổi nó thành một cái gì đó như thế này:

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);       
}

Việc thực hiện đã thay đổi mạnh mẽ nhưng người gọi ChangeStatusphương thức không biết về các chi tiết triển khai cơ bản và không có lý do gì để thay đổi chính nó.

Đây là một ví dụ về thực thể mô hình miền phong phú, IMHO.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.