丰富与贫血领域模型

Sam*_*Sam 83 domain-model anemic-domain-model rich-domain-model

我决定是否应该使用富域模型而不是贫穷领域模型,并寻找两者的好例子.

我一直使用Anemic Domain Model构建Web应用程序,由Service - > Repository - > Storage层系统支持,使用FluentValidation进行BL验证,并将我的所有BL放在Service层中.

我读过Eric Evan的DDD书,他(以及Fowler和其他人)似乎认为Anemic Domain Models是一种反模式.

所以我真的很想了解这个问题.

此外,我真的在寻找一个富域模型的好(基本)示例,以及它提供的贫血域模型的好处.

geo*_*and 51

Bozhidar Bozhanov似乎在这篇博客文章中支持贫血模型.

以下是他提出的摘要:

  • 域对象不应该是spring(IoC)管理的,它们不应该有DAO或与其中注入的基础结构相关的任何内容

  • 域对象具有它们依赖于hibernate设置的域对象(或持久性机制)

  • 域对象执行业务逻辑,因为DDD的核心思想是,但这不包括数据库查询或CRUD - 仅对对象内部状态的操作

  • 很少需要DTO - 在大多数情况下,域对象本身就是DTO(这节省了一些样板代码)

  • 服务执行CRUD操作,发送电子邮件,协调域对象,基于多个域对象生成报告,执行查询等.

  • 服务(应用程序)层不是那么薄,但不包括域对象固有的业务规则

  • 应该避免代码生成.应该使用抽象,设计模式和DI来克服代码生成的需要,并最终 - 摆脱代码重复.

UPDATE

我最近阅读了这篇文章,其中作者提倡遵循一种混合方法 - 域对象可以仅根据其状态回答各种问题(在完全贫血模型的情况下可能会在服务层中完成)

  • 我无法从那篇文章中摘录出Bozho似乎赞成贫血领域模型的观点._服务(应用程序)层不是那么薄,但不包括域对象_固有的业务规则.据我所知,域对象应包含它们固有的业务逻辑,但它们不应包含任何其他_Infrastructure_逻辑.这种方法对我来说似乎不像是一个贫血的领域模型. (10认同)
  • 另外这个:_domain对象执行业务逻辑,因为DDD的核心思想是,但这不包括数据库查询或仅对对象内部状态的CRUD操作.这些陈述似乎根本不支持贫血领域模型.他们只声明_infrastructure logic_不应该耦合到域对象.至少这是我理解的. (7认同)

Ren*_*ink 50

不同之处在于贫血模型将逻辑与数据分开.该逻辑通常放在名为类**Service,**Util,**Manager,**Helper等.这些类实现数据解释逻辑,因此将数据模型作为参数.例如

public BigDecimal calculateTotal(Order order){
...
}
Run Code Online (Sandbox Code Playgroud)

而富域域方法通过将数据解释逻辑放入富域模型来反转这一点.因此,它将逻辑和数据放在一起,富域模型看起来像这样:

order.getTotal();
Run Code Online (Sandbox Code Playgroud)

这对对象一致性有很大影响.由于数据解释逻辑包装数据(数据只能通过对象方法访问),因此方法可以对其他数据的状态变化做出反应 - >这就是我们所说的行为.

在贫血模型中,数据模型不能保证它们处于合法状态,而在富域模型中它们可以.丰富的域模型应用OO原则,如封装,信息隐藏以及将数据和逻辑结合在一起,因此贫血模型是从OO角度出发的反模式.

如需更深入的了解,请查看我的博客https://www.link-intersystems.com/blog/2011/10/01/anemic-vs-rich-domain-models/

  • 假设计算订单的总价格涉及:1)应用折扣,该折扣取决于客户是可能的许多忠诚度计划之一的成员.2)根据商店运行的当前营销活动,对包含特定项目组的订单应用折扣.3)计算税额,其中税额取决于订单的每个特定项目.在您看来,所有这些逻辑都属于哪里?你能给一个简单的伪代码示例吗?谢谢! (14认同)
  • @Nik在富模型中,Order将引用Customer对象,Customer对象将引用Loyalty Program.因此,Order可以访问所需的所有信息,而无需显式引用服务和存储库等内容来获取该信息.但是,遇到周期性参考的情况似乎很容易.即订单参考客户,客户有一个所有订单的列表.我认为这可能是人们现在更喜欢Anemic的部分原因. (4认同)
  • @crush 您描述的方法非常有效。有一个问题。可能我们将实体存储在数据库中。因此,要计算订单总额,我们必须从数据库中获取订单、客户、忠诚度计划、营销活动、税收表。还要考虑,客户有一系列订单,忠诚度计划有一系列客户等等。如果我们天真地获取所有这些,我们最终将在 RAM 中加载整个数据库。这当然是不可行的,所以我们只能从数据库加载相关数据...... 1/2 (3认同)
  • @Nik “如果我们本机获取所有这些,我们最终将在 RAM 中加载整个数据库。” 这也是我认为丰富模型的主要缺点之一。丰富模型很好,直到您的域变得庞大而复杂,然后您开始遇到基础设施限制。不过,这就是延迟加载 ORM 可以提供帮助的地方。找到一个好的,你可以保留丰富的模型,而无需将整个 DB 加载到内存中,当你只需要它的 1/20 时。也就是说,经过多年在贫血和富人之间来回穿梭之后,我自己倾向于将贫血模型与 CQRS 结合使用。 (3认同)
  • 要考虑的另一件事是您的业务域逻辑所在的位置。我认为越来越多的开发人员正在将其移出数据库,并移至其所属的应用程序。但是,如果您陷入公司要求业务逻辑保留在数据库层(存储过程)中的情况,那么您几乎肯定不会从在富域模型中添加该逻辑中受益。实际上,您可能只是设置自己陷入了冲突,在这些冲突中,存储过程的规则与应用程序的域层不同。 (2认同)

小智 39

我的观点是这样的:

贫血域模型=映射到对象的数据库表(仅字段值,没有实际行为)

富域模型=暴露行为的对象集合

如果你想创建一个简单的CRUD应用程序,也许一个带有经典MVC框架的贫血模型就足够了.但是如果你想实现某种逻辑,贫血模型意味着你不会做面向对象的编程.

*请注意,对象行为与持久性无关.不同的层(数据映射器,存储库等)负责持久化域对象.

  • 很抱歉我的无知,但如果您将所有与实体相关的逻辑放在课堂中,富域模型如何遵循SOLID原则.这违反了SOLID原则,即'S',它代表单一责任,即一个班级应该只做一件事并且做得对. (5认同)
  • @redigaffi这取决于你如何定义"一件事".考虑一个具有两个属性和两个方法的类:`x`,`y`,`sum`和`difference`.那是四件事.或者你可以说它是加法和减法(两件事).或者你可以说它是数学(一件事).有很多关于如何在应用SRP时找到平衡的博客文章.这是一个:https://hackernoon.com/you-dont-understand-the-single-responsibility-principle-abfdd005b137 (5认同)
  • 在DDD中,单一可重用性意味着类/模型可以管理自己的状态,而不会对整个系统的其余部分造成任何副作用。根据我的经验,任何其他定义只会导致乏味的哲学辩论。 (2认同)

Raz*_*aul 11

首先,我复制粘贴了这篇文章的答案 http://msdn.microsoft.com/en-gb/magazine/dn385704.aspx

图1显示了一个Anemic Domain Model,它基本上是一个带有getter和setter的模式.

Figure 1 Typical Anemic Domain Model Classes Look Like Database Tables

public class Customer : Person
{
  public Customer()
  {
    Orders = new List<Order>();
  }
  public ICollection<Order> Orders { get; set; }
  public string SalesPersonId { get; set; }
  public ShippingAddress ShippingAddress { get; set; }
}
public abstract class Person
{
  public int Id { get; set; }
  public string Title { get; set; }
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public string CompanyName { get; set; }
  public string EmailAddress { get; set; }
  public string Phone { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

在这个更丰富的模型中,Customer的公共表面不是简单地公开要读取和写入的属性,而是由显式方法组成.

Figure 2 A Customer Type That’s a Rich Domain Model, Not Simply Properties

public class Customer : Contact
{
  public Customer(string firstName, string lastName, string email)
  {
    FullName = new FullName(firstName, lastName);
    EmailAddress = email;
    Status = CustomerStatus.Silver;
  }
  internal Customer()
  {
  }
  public void UseBillingAddressForShippingAddress()
  {
    ShippingAddress = new Address(
      BillingAddress.Street1, BillingAddress.Street2,
      BillingAddress.City, BillingAddress.Region,
      BillingAddress.Country, BillingAddress.PostalCode);
  }
  public void CreateNewShippingAddress(string street1, string street2,
   string city, string region, string country, string postalCode)
  {
    ShippingAddress = new Address(
      street1,street2,
      city,region,
      country,postalCode)
  }
  public void CreateBillingInformation(string street1,string street2,
   string city,string region,string country, string postalCode,
   string creditcardNumber, string bankName)
  {
    BillingAddress = new Address      (street1,street2, city,region,country,postalCode );
    CreditCard = new CustomerCreditCard (bankName, creditcardNumber );
  }
  public void SetCustomerContactDetails
   (string email, string phone, string companyName)
  {
    EmailAddress = email;
    Phone = phone;
    CompanyName = companyName;
  }
  public string SalesPersonId { get; private set; }
  public CustomerStatus Status { get; private set; }
  public Address ShippingAddress { get; private set; }
  public Address BillingAddress { get; private set; }
  public CustomerCreditCard CreditCard { get; private set; }
}
Run Code Online (Sandbox Code Playgroud)

  • 创建对象并使用新创建的对象分配属性的方法存在问题.它们使代码的可扩展性和灵活性降低.1)如果代码使用者想要创建不是`Address`,但是`ExtendedAddress`,继承自`Address`,还有几个额外的属性怎么办?2)或者更改`CustomerCreditCard`构造函数参数以使用`BankID`而不是`BankName`? (2认同)

Luk*_*ett 8

当我过去编写整体桌面应用程序时,我构建了丰富的域模型,并喜欢构建它们。

现在,我编写了微小的HTTP微服务,其中包含的代码很少,包括贫乏的DTO。

我认为DDD和这种贫乏的争论可以追溯到整体桌面或服务器应用时代。我记得那个时代,我同意贫血模型是奇怪的。我建立了一个大型的整体外汇交易应用程序,但没有模型,真的,这太可怕了。

对于微服务而言,具有丰富行为的小型服务可以说是域内可组合的模型和集合。因此,微服务实现本身可能不需要进一步的DDD。微服务应用程序可以是域。

订单微服务可能只有很少的功能,以RESTful资源或通过SOAP或其他方式表示。订单微服务代码可能非常简单。

更大,更单一的单一(微)服务,尤其是将其保持在RAM中的模型,可能会受益于DDD。


joc*_*cki 7

富域类的一个好处是,每次在任何层中引用对象时,都可以调用它们的行为(方法).此外,您倾向于编写协作一致的小型分布式方法.在贫血领域类中,您倾向于编写通常由用例驱动的胖程序方法(在服务层中).与富域类相比,它们通常难以维护.

具有行为的域类的示例:

class Order {

     String number

     List<OrderItem> items

     ItemList bonus

     Delivery delivery

     void addItem(Item item) { // add bonus if necessary }

     ItemList needToDeliver() { // items + bonus }

     void deliver() {
         delivery = new Delivery()
         delivery.items = needToDeliver()
     }

}
Run Code Online (Sandbox Code Playgroud)

方法needToDeliver()将返回需要交付的项目列表,包括奖金.它可以在类中,从另一个相关类或另一个层中调用.例如,如果您传递Order给视图,那么您可以使用needToDeliver()选择Order来显示用户确认的项目列表,然后单击"保存"按钮以保留该项目Order.

回应评论

这是我如何使用控制器中的域类:

def save = {
   Order order = new Order()
   order.addItem(new Item())
   order.addItem(new Item())
   repository.create(order)
}
Run Code Online (Sandbox Code Playgroud)

创建Order和它LineItem在一个事务中.如果LineItem无法创建其中一个,则不会Order创建.

我倾向于拥有代表单个事务的方法,例如:

def deliver = {
   Order order = repository.findOrderByNumber('ORDER-1')
   order.deliver()       
   // save order if necessary
}
Run Code Online (Sandbox Code Playgroud)

内部的任何内容deliver()都将作为一个单一的交易执行.如果我需要在单个事务中执行许多不相关的方法,我会创建一个服务类.

为了避免延迟加载异常,我使用JPA 2.1命名实体图.例如,在交付屏幕的控制器中,我可以创建加载delivery属性和忽略的方法bonus,例如repository.findOrderByNumberFetchDelivery().在奖金屏幕中,我调用另一个加载bonus属性和忽略的方法delivery,例如repository.findOrderByNumberFetchBonus().这需要dicipline,因为我仍然无法调用deliver()内部奖金屏幕.

  • 域模型行为不应包含持久性逻辑(包括事务).它们应该是可测试的(在单元测试中)而不连接到数据库.事务范围是服务层或持久层的职责. (4认同)
  • 交易范围如何? (2认同)
  • 那么延迟加载怎么样? (2认同)

Pau*_*-AG 5

我认为问题的根源在于错误的二分法。如何提取这两种模型:丰富的和“贫乏的”并将它们相互对比?我认为只有当你对什么是类有错误的想法时才有可能。我不确定,但我想我在 YouTube 上的 Bozhidar Bozhanov 视频之一中找到了它。类不是数据+该数据上的方法。这是完全无效的理解,导致类分为两类:仅数据,因此贫血模型数据+方法- 如此丰富的模型(更正确的是,还有第三类:甚至仅方法)。

事实是,类是某种本体模型中的一个概念、一个词、一个定义、一个术语、一个想法,它是一个DENOTAT。这种理解消除了错误的二分法:你不能只有贫血模型或只有丰富模型,因为这意味着你的模型不充分,它与现实无关:有些概念只有数据,有些概念只有方法,有些概念只有方法。其中有混合的。因为在这种情况下,我们试图描述一些类别、对象集、关系、类概念,并且正如我们所知,一些概念只是过程(方法),其中一些只是属性集(数据),它们是与属性的关系(混合)。

我认为一个足够的应用程序应该包含所有类型的类,并避免狂热地自我限制为一种模型。不管逻辑如何表示:用代码或可解释的数据对象(如Free Monads),无论如何:我们应该有表示过程、逻辑、关系、属性、特征、数据等的类(概念、指示符),而不是尝试避免其中一些或将所有这些减少为一种。

因此,我们可以将逻辑提取到另一个类并将数据保留在原始类中,但这没有意义,因为某些概念可以包含属性和关系/过程/方法,并且将它们分开会在两个名称下重复该概念,这可以是简化为模式:“对象属性”和“对象逻辑”。在过程语言和函数语言中这很好,因为它们有局限性,但对于允许你描述各种概念的语言来说,这是过度的自我约束。