【问题标题】:Rich vs Anemic Domain Model [closed]富域模型与贫血域模型 [关闭]
【发布时间】:2014-06-12 10:04:51
【问题描述】:

我正在决定是否应该使用富域模型而不是贫血域模型,并寻找两者的好例子。

我一直在使用贫血域模型构建 Web 应用程序,由 Service --> Repository --> Storage 层系统支持,使用 FluentValidation 进行 BL 验证,并将我所有的 BL 放在 Service 层中。

我读过 Eric Evan 的 DDD 书,他(以及 Fowler 和其他人)似乎认为贫血域模型是一种反模式。

所以我真的很想深入了解这个问题。

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

【问题讨论】:

标签: domain-model anemic-domain-model rich-domain-model


【解决方案1】:

Bozhidar Bozhanov 在this 博客文章中似乎支持贫血模型。

这是他提出的摘要:

  • 域对象不应由 Spring (IoC) 管理,它们不应有 DAO 或任何与基础设施相关的注入其中

  • 域对象具有由休眠(或持久性机制)设置的它们所依赖的域对象

  • 领域对象执行业务逻辑,就像 DDD 的核心思想一样,但这不包括数据库查询或 CRUD——仅对对象的内部状态进行操作

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

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

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

  • 应避免生成代码。应该使用抽象、设计模式和 DI 来克服代码生成的需要,并最终消除代码重复。

更新

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

【讨论】:

  • 我无法从那篇文章中提取出 Bozho 似乎支持贫血域模型的观点。 服务(应用程序)层没有那么薄,但不包括领域对象固有的业务规则。我的理解是,域对象应该包含它们固有的业务逻辑,但它们不应该包含任何其他 infrastructure 逻辑。这种方法在我看来根本不像是一个贫血的领域模型。
  • 还有这个:领域对象执行业务逻辑,就像DDD的核心思想一样,但这不包括数据库查询或CRUD——只是对对象内部状态的操作。这些陈述似乎根本不支持贫血域模型。他们只声明基础设施逻辑不应该耦合到领域对象。至少我是这么理解的。
  • @Utku 在我看来,Bozho 提倡两种模型之间的某种混合似乎相当明显,我认为这种混合更接近于贫血模型而不是富模型。
【解决方案2】:

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

具有行为的域类示例:

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

}

方法needToDeliver() 将返回需要交付的项目列表,包括奖金。它可以在类内部、从另一个相关类或从另一个层调用。例如,如果您传递Order 来查看,那么您可以使用选中的Order 中的needToDeliver() 来显示用户需要确认的项目列表,然后再点击保存按钮以持久化Order

回复评论

这就是我从控制器中使用域类的方式:

def save = {
   Order order = new Order()
   order.addItem(new Item())
   order.addItem(new Item())
   repository.create(order)
}

Order 及其LineItem 的创建在一个事务中。如果无法创建LineItem 之一,则不会创建Order

我倾向于使用表示单个事务的方法,例如:

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

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

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

【讨论】:

  • 事务范围如何?
  • 域模型行为不应包含持久性逻辑(包括事务)。它们应该是可测试的(在单元测试中)而不连接到数据库。事务范围是服务层或持久层的职责。
  • 那么延迟加载怎么样?
  • 在单元测试中创建域类实例时,它们不是托管状态,因为它们是普通对象。所有行为都可以正确测试。
  • 当您期望来自服务层的域对象时会发生什么?那不是管理的吗?
【解决方案3】:

我的观点是这样的:

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

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

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

*请注意,对象行为与持久性无关。不同的层(Data Mappers、Repositories 等)负责持久化领域对象。

【讨论】:

  • 抱歉我的无知,但是如果您将所有与实体相关的逻辑放在类中,富域模型如何遵循 SOLID 原则。这违反了 SOLID 原则,确切地说是“S”,它代表单一责任,即一个班级应该只做一件事并把它做好。
  • @redigaffi 这取决于你如何定义“一件事”。考虑一个具有两个属性和两个方法的类:xysumdifference。这是四件事。或者你可以说它是加法和减法(两件事)。或者你可以争辩说这是数学(一件事)。有很多关于如何在应用 SRP 中找到平衡的博客文章。这是一个:hackernoon.com/…
  • 在 DDD 中,单一职责意味着一个类/模型可以管理它自己的状态,而不会对整个系统的其余部分造成任何副作用。根据我的经验,任何其他定义只会导致乏味的哲学辩论。
【解决方案4】:

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

图 1 显示了一个贫血域模型,它基本上是一个包含 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; }
}

在这个更丰富的模型中,而不是简单地公开要读取和写入的属性, 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; }
}

【讨论】:

  • 创建对象并为新创建的对象分配属性的方法存在问题。它们使代码的可扩展性和灵活性降低。 1) 如果代码使用者想要创建的不是Address,而是ExtendedAddress,继承自Address,并带有几个附加属性怎么办? 2)或者将CustomerCreditCard构造函数参数改为BankID而不是BankName
  • 什么是创建地址需要额外的服务而不是组成对象?剩下的就是方法注入来获得这些服务。如果有很多服务怎么办?
【解决方案5】:

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

public BigDecimal calculateTotal(Order order){
...
}

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

order.getTotal();

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

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

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

【讨论】:

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

这是一个可能有帮助的例子:

贫血

class Box
{
    public int Height { get; set; }
    public int Width { get; set; }
}

无贫血

class Box
{
    public int Height { get; private set; }
    public int Width { get; private set; }

    public Box(int height, int width)
    {
        if (height <= 0) {
            throw new ArgumentOutOfRangeException(nameof(height));
        }
        if (width <= 0) {
            throw new ArgumentOutOfRangeException(nameof(width));
        }
        Height = height;
        Width = width;
    }

    public int area()
    {
       return Height * Width;
    }
}

【讨论】:

  • 这看起来可以转换为 ValueObject vs Entity。
  • 只是维基百科的复制粘贴,没有任何解释
  • 谁写得更早? @wst
  • @AlirezaRahmaniKhalili 根据维基百科的历史,他们是第一个......除非,我不明白你的问题。
【解决方案7】:

贫血域模型对于 ORM 和通过网络轻松传输(所有商业应用程序的命脉)很重要,但 OO 对于封装和简化代码的“事务/处理”部分非常重要。

因此,重要的是能够识别并从一个世界转换到另一个世界。

将 Anemic 模型命名为 AnemicUser 或 UserDAO 等,以便开发人员知道可以使用更好的类,然后为非 Anemic 类提供适当的构造函数

User(AnemicUser au)

和适配器方法来创建用于传输/持久性的贫血类

User::ToAnemicUser() 

旨在在传输/持久性之外的任何地方使用非贫血用户

【讨论】:

    【解决方案8】:

    当我过去编写单体桌面应用程序时,我构建了丰富的域模型,过去很享受构建它们。

    现在我编写微型 HTTP 微服务,代码尽可能少,包括贫乏的 DTO。

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

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

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

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

    【讨论】:

    • 您有代表您当前最先进技术的 HTTP 微服务代码示例吗?不要求您写任何东西,如果您有任何可以指出的内容,只需分享链接。谢谢。
    【解决方案9】:

    DDD 的经典方法并未声明不惜一切代价避免贫血模型与富模型。但是,MDA 仍然可以应用所有 DDD 概念(有界上下文、上下文映射、值对象等),但在所有情况下都使用 Anemic 与 Rich 模型。在许多情况下,使用域服务在一组域聚合中编排复杂的域用例是一种比仅从应用层调用聚合更好的方法。与经典 DDD 方法的唯一区别是所有验证和业务规则都驻留在哪里?有一种称为模型验证器的新结构。验证器在任何用例或域工作流发生之前确保完整输入模型的完整性。聚合的根和子实体是贫乏的,但每个实体都可以根据需要由其根验证器调用自己的模型验证器。验证器仍然遵守 SRP,易于维护且可进行单元测试。

    这种转变的原因是我们现在更多地转向 API 优先而不是 UX 优先的微服务方法。 REST 在这方面发挥了非常重要的作用。传统的 API 方法(因为 SOAP)最初专注于基于命令的 API 与 HTTP 动词(POST、PUT、PATCH、GET 和 DELETE)。基于命令的 API 非常适合 Rich Model 面向对象的方法,并且仍然非常有效。然而,简单的基于 CRUD 的 API,虽然它们可以适应富模型,但更适合简单的贫乏模型、验证器和域服务来协调其余部分。

    我喜欢 DDD 所提供的一切,但有时你需要稍微扩展它以适应不断变化和更好的架构方法。

    【讨论】:

      【解决方案10】:

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

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

      我认为一个适当的应用程序应该包括所有类型的类,并避免狂热地自我限制在一个模型上。不管逻辑是如何表示的:用代码或用可解释的数据对象(如Free Monads),无论如何:我们应该有表示过程、逻辑、关系、属性、特征、数据等的类(概念、表示)和不要试图避免其中的一些或将它们全部归结为一种。

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

      【讨论】:

        猜你喜欢
        • 2020-01-29
        • 2010-12-20
        • 2014-01-05
        • 2018-12-14
        • 1970-01-01
        • 2010-11-04
        • 1970-01-01
        • 2010-12-26
        • 2012-02-04
        相关资源
        最近更新 更多