富vs贫血域模型


93

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

我一直在使用Anemic域模型构建Web应用程序,并以Service- >存储库-> Storage Layer系统为后盾,使用FluentValidation进行BL验证,并将我所有的BL都放入Service层。

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

所以我只是真的想对这个问题有一些了解。

另外,我确实在寻找Rich Domain模型的一些好(基本)示例,以及它提供的Anemic Domain Model带来的好处。


您可能还想查看该博客,
Japheth Ongeri-inkalimeva

14
DDD> ADMADM> DDDDDD> ADMADM> DDDADM + DDD ... DDD / ADM,或者如何不同意软件设计
sp00m

下面是如何避免贫血域模型的示例:medium.com/@wrong.about/...
瓦迪姆Samokhin

11
有趣的是,这个问题可以通过指向真实组织资助的真实世界项目的单个链接来回答。五年后,IMO没有好的答案。谈话很便宜。给我看代码。
Mateusz Stefek '19

Answers:


57

区别在于,贫血模型将逻辑与数据分开。该逻辑通常放在名为类**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/


15
假设计算订单的总价涉及:1)应用折扣,这取决于客户是否是许多忠诚度计划之一的成员。2)根据商店当前的市场营销活动,对包含特定项目组的订单应用折扣。3)计算税额,税额取决于订单的每个特定项目。您认为,所有这些逻辑将归于何处?您能否举一个简单的伪代码示例。谢谢!
Nik

4
@Nik在丰富模型中,“订单”将引用“客户”对象,而“客户”对象将引用“忠诚度计划”。因此,该订单将可以访问所需的所有信息,而无需显式引用从中获取信息的服务和存储库之类的东西。但是,似乎很容易遇到循环引用发生的情况。即订单引用客户,客户有所有订单的列表。我认为这可能部分是为什么人们现在喜欢Anemic的原因。
暗恋

3
@crush您描述的方法非常有效。有一个收获。我们可能将实体存储在数据库中。因此,要计算订单总数,我们必须从数据库中获取订单,客户,忠诚度计划,市场营销活动,税收表。还请考虑,客户具有订单集合,忠诚度计划具有客户集合,依此类推。如果天真地获取所有这些,最终将把整个数据库加载到RAM中。当然这是不可行的,所以我们只能从数据库中加载相关数据... 1/2
尼克

3
@Nik“如果从本地获取所有这些信息,我们最终将整个数据库加载到RAM中。” 这也是我认为丰富模型的主要缺点之一。丰富的模型很好,直到您的域变得庞大和复杂,然后您开始遇到基础架构限制。不过,这是懒加载ORM可以提供帮助的地方。找到一个好的模型,您就可以保留丰富的模型,而只需要其1/20的时候就不会将整个数据库加载到内存中。就是说,我在贫血和富人之间来回多年之后,倾向于自己在CQRS中使用贫血模型。
暗恋

2
要考虑的另一件事是您的业务域逻辑所在的位置。我认为越来越多的开发人员正在将其移出数据库,并移至其所属的应用程序。但是,如果您陷入公司要求业务逻辑保留在数据库层(存储过程)中的情况,那么几乎可以肯定的是,您还不能从在富域模型中添加该逻辑中受益。事实上,你可能只是在和自己碰上在存储过程中有比你的应用程序的域层不同的规则冲突...
粉碎

53

Bozhidar Bozhanov在博客文章中似乎主张贫血模型。

这是他介绍的摘要:

  • 域对象不应由Spring(IoC)管理,它们不应具有DAO或与之相关的任何基础架构

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

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

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

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

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

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

更新

我最近读了这篇文章,在那儿,作者提倡采用一种混合方法-领域对象可以仅根据状态来回答各种问题(在完全贫乏的模型中,这可以在服务层完成)


11
我从那篇文章中无法得出博佐似乎在主张贫血领域模型的观点。服务(应用程序)层并不薄,但是不包括域对象固有的业务规则。我了解的是,域对象应包含其固有的业务逻辑,但不应包含任何其他基础结构逻辑。在我看来,这种方法似乎根本不是贫血领域模型。
Utku

8
还有一个:域对象执行业务逻辑,就像DDD的核心思想一样,但这不包括数据库查询或CRUD –仅对对象的内部状态进行操作。这些陈述似乎根本不赞成贫血领域模型。它们仅声明基础架构逻辑不应与域对象耦合。至少这是我的理解。
Utku

@Utku在我看来,很明显,Bozho主张在两种模型之间进行某种混合,我认为混合比富模型更接近贫血模型。
geoand

41

我的观点是:

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

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

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

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


5
很抱歉,我的无知,但是如果您将所有与Entity相关的逻辑放在类中,那么丰富域模型将如何遵循SOLID原则。这违反了SOLID原则,即“ S”,它代表单一责任,即一个类只能做一件事情并且正确地做。
redigaffi

5
@redigaffi这取决于您如何定义“一件事”。考虑具有两个属性和两个方法的类:xysumdifference。那是四件事。或者,您可以说这是加法和减法(两件事)。或者,您可以说这是数学(一件事)。那里有许多博客文章,介绍如何在应用SRP时找到平衡。这是一个:hackernoon.com/…–
Rainbolt

2
在DDD中,单一用途意味着类/模型可以管理自己的状态,而不会对整个系统的其余部分造成任何副作用。根据我的经验,任何其他定义只会导致乏味的哲学辩论。
ZombieTfk

12

首先,我复制粘贴了这篇文章的答案 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; }
}

2
创建对象并为新创建的对象分配属性的方法存在问题。它们使代码的可扩展性和灵活性降低。1)如果代码使用者想创建不是Address,而是ExtendedAddress继承自Address,并具有其他一些属性,该怎么办?2)或更改CustomerCreditCard构造函数参数以BankID代替BankName
莱特曼

什么是创建地址,而不是组成对象的服务?您仅需进行方法注入即可获得这些服务。如果有很多服务怎么办?
美眉

8

富域类的好处之一是,只要您在任何层中都有对对象的引用,就可以调用它们的行为(方法)。另外,您倾向于编写小型的分布式方法,这些方法可以协同工作。在贫血症领域类中,您倾向于编写通常由用例驱动的胖过程方法(在服务层中)。与丰富的域类相比,它们通常较难维护。

具有行为的域类的示例:

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查看,则可以使用needToDeliver()selected Order来显示要由用户确认的项目列表,然后用户单击保存按钮以保留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()在奖金屏幕内致电。


1
交易范围如何?
KBoom

5
域模型行为不应包含持久性逻辑(包括事务)。它们应该是可测试的(在单元测试中),而无需连接到数据库。事务范围是服务层或持久层的责任。
jocki 2014年

1
那懒加载呢?
KBoom

在单元测试中创建域类实例时,它们不处于托管状态,因为它们是普通对象。可以正确测试所有行为。
jocki 2014年

当您期望服务层提供域对象时会发生什么?那不是吗?
kboom 2014年

8

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

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

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

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

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

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


您是否有任何代表您当前技术水平的HTTP微服务代码示例?不要求您编写任何内容,如果您有任何指向的内容,只需共享链接。谢谢。
Casey Plummer

3

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

的确,类是某些本体模型中的一个概念,一个单词,一个定义,一个术语,一个想法,它是DENOTAT。这种理解消除了错误的二分法:您不能只有贫血模型或富人模型,因为这意味着您的模型不够用,与现实不相关:某些概念仅包含数据,某些概念仅包含方法,有些他们混合在一起。因为在这种情况下,我们试图描述一些类别,对象集,关系,带有类的概念,并且我们知道,一些概念仅是过程(方法),一些仅是属性集(数据),一些它们是与属性的关系(混合)。

我认为适当的应用程序应包括各种类,并避免狂热地自我限制为仅一个模型。无论如何,逻辑是如何表示的:无论如何使用代码或可解释的数据对象(如Free Monads):我们应该具有表示流程,逻辑,关系,属性,特征,数据等的类(概念,符号),而不是设法避免其中一些或将它们全部减少为一种。

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


1

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

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

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

User(AnemicUser au)

和适配器方法来创建贫血类以进行运输/持久性

User::ToAnemicUser() 

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


-1

以下示例可能会有所帮助:

贫血

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

谁写得更快?@wst
Alireza Rahmani Khalili

根据维基百科的历史记录,@ AlirezaRahmaniKhalili是第一个...除非,我不明白您的问题。
WST

-1

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

发生这种转变的原因是,我们现在正朝着微服务的API优先而非UX优先的方向发展。REST在其中扮演了非常重要的角色。传统的API方法(由于SOAP)最初固定在基于命令的API与HTTP动词(POST,PUT,PATCH,GET和DELETE)上。基于命令的API非常适合Rich Model面向对象的方法,并且仍然非常有效。但是,尽管基于CRUD的简单API可以放入Rich模型中,但更适合与简单的贫血模型,验证器和域服务来协调其余部分。

我喜欢DDD所提供的所有功能,但是有时候您需要对其进行扩展以适应不断变化的更好的体系结构。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.