富域与贫血域模型

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

I have been building web applications using an Anemic Domain Model, backed by a 服务—— > 资源库—— > 存储 layer system, using 验证 for BL validation, and putting all of my BL in the Service layer.

I have read Eric Evan's DDD book, and he (along with Fowler and others) seems to think Anemic Domain Models are an anti-pattern.

所以我真的很想了解一下这个问题。

此外,我真的在寻找一些好的(基本的)丰富域模型的例子,以及它所提供的优于贫血域模型的好处。

56044 次浏览

Bozhidar Bozhanov seems to argue in favor of the anemic model in this blog post.

Here is the summary he presents:

  • 域对象不应该被 Spring (IoC)管理,它们不应该注入 DAO 或任何与基础设施相关的内容

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

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

  • there is rarely need of DTOs – the domain objects are the DTOs themselves in most cases (which saves some boilerplate code)

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

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

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

更新

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

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

An example of domain classes with behaviours:

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


}

Method needToDeliver() will return list of items that need to be delivered including bonus. It can be called inside the class, from another related class, or from another layer. For example, if you pass Order to view, then you can use needToDeliver() of selected Order to display list of items to be confirmed by user before they click on save button to persist the 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()内奖金屏幕。

我的观点是:

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

富域模型 = 公开行为的对象的集合

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

*Note that object behavior has nothing to do with persistence. A different layer (Data Mappers, Repositories e.t.c.) is responsible for persisting domain objects.

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

在这个更丰富的模型中,与其简单地公开要读写的属性, 客户的公共表面是由明确的方法组成的

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

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

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

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

order.getTotal();

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

In an anemic model the data models can not guarantee that they are in a legal state while in a rich domain model they can. A rich domain model applies OO principles like encapsulation, information hiding and bringing data and logic together and therefore a anemic model is an anti pattern from an OO perspective.

For a deeper insight take a look at my blog https://www.link-intersystems.com/blog/2011/10/01/anemic-vs-rich-domain-models/

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

贫血

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

缺乏活力的域模型对于 ORM 和通过网络(所有商业应用的生命线)的容易传输很重要,但是 OO 对于封装和简化代码的“事务/处理”部分非常重要。

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

将贫血模型命名为 AnemicUser 或 UserDAO 等,这样开发人员就知道有更好的类可以使用,然后为无贫血类提供一个合适的构造函数

User(AnemicUser au)

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

User::ToAnemicUser()

Aim to use the none Anemic User everywhere outside of transport/persistence

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

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

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

With microservices, the small services with their rich behaviour, are arguably the composable models and aggregates within a domain. So the microservice implementations themselves may not require further DDD. The microservice application may be the domain.

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

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

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

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

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

我认为问题的根源在于错误的二分法。怎样才能提取出这两种模型: 丰富的和“贫血的”,并将它们相互对比?我认为只有当你对 什么是类有一个错误的想法时才有可能。我不确定,但我想我是在 Youtube 上的 Bozhidar Bozhanov 的一个视频里找到的。类不是此数据上的 data + 方法。这是完全无效的理解,导致类划分为两类: 只有数据,所以 贫血模型data + methods-如此丰富的模型(更正确地说,有第三类: 甚至只有方法)。

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

我认为一个适当的应用程序应该包括所有类,并避免狂热地自我限制到只有一个模型。无论如何,逻辑是如何表示的: 使用代码或可解释的数据对象(如 免费单子) ,无论如何: 我们应该有代表过程、逻辑、关系、属性、特性、数据等的类(概念、表示符) ,而不是试图避免其中的一些,或者将它们全部减少到一种类型。

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