我正在决定是否应该使用富域模型而不是贫血域模型,并寻找两者的好例子。
我一直在使用贫血域模型构建 Web 应用程序,由服务 --> 存储库 --> 存储层系统支持,使用FluentValidation进行 BL 验证,并将我的所有 BL 放在服务层中。
我读过 Eric Evan 的 DDD 书,他(连同 Fowler 和其他人)似乎认为贫血域模型是一种反模式。
所以我真的很想深入了解这个问题。
此外,我真的在寻找富域模型的一些好的(基本)示例,以及它提供的贫血域模型的好处。
我正在决定是否应该使用富域模型而不是贫血域模型,并寻找两者的好例子。
我一直在使用贫血域模型构建 Web 应用程序,由服务 --> 存储库 --> 存储层系统支持,使用FluentValidation进行 BL 验证,并将我的所有 BL 放在服务层中。
我读过 Eric Evan 的 DDD 书,他(连同 Fowler 和其他人)似乎认为贫血域模型是一种反模式。
所以我真的很想深入了解这个问题。
此外,我真的在寻找富域模型的一些好的(基本)示例,以及它提供的贫血域模型的好处。
不同之处在于贫血模型将逻辑与数据分开。逻辑通常放在名为**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/
Bozhidar Bozhanov 在这篇博文中似乎支持贫血模型。
以下是他提出的摘要:
域对象不应该由 Spring (IoC) 管理,它们不应该有 DAO 或任何与基础设施相关的东西注入其中
域对象具有它们所依赖的域对象,由休眠(或持久性机制)设置
领域对象执行业务逻辑,就像 DDD 的核心思想一样,但这不包括数据库查询或 CRUD——仅对对象的内部状态进行操作
很少需要 DTO——在大多数情况下,域对象本身就是 DTO(这节省了一些样板代码)
服务执行 CRUD 操作、发送电子邮件、协调域对象、基于多个域对象生成报告、执行查询等。
服务(应用程序)层没有那么薄,但不包括领域对象固有的业务规则
应避免代码生成。应该使用抽象、设计模式和 DI 来克服代码生成的需要,并最终消除代码重复。
更新
我最近读了这篇文章,作者提倡遵循一种混合方法——域对象可以仅根据它们的状态来回答各种问题(在完全贫血模型的情况下,可能会在服务层完成)
我的观点是这样的:
贫血域模型 = 映射到对象的数据库表(只有字段值,没有实际行为)
富域模型 = 暴露行为的对象集合
如果你想创建一个简单的 CRUD 应用程序,也许一个带有经典 MVC 框架的贫血模型就足够了。但是如果你想实现某种逻辑,贫血模型意味着你不会做面向对象的编程。
*请注意,对象行为与持久性无关。不同的层(数据映射器、存储库等)负责持久化域对象。
当我过去编写单体桌面应用程序时,我构建了丰富的域模型,过去常常喜欢构建它们。
现在我编写微小的 HTTP 微服务,代码尽可能少,包括贫乏的 DTO。
我认为 DDD 和这种贫乏的争论可以追溯到单体桌面或服务器应用程序时代。我记得那个时代,我同意贫血模型很奇怪。我构建了一个大型的单一外汇交易应用程序,但没有模型,真的,太可怕了。
对于微服务,具有丰富行为的小型服务可以说是域内的可组合模型和聚合。所以微服务实现本身可能不需要进一步的 DDD。微服务应用程序可能是域。
订单微服务可能只有很少的功能,以 RESTful 资源或通过 SOAP 或其他方式表示。订单微服务代码可能非常简单。
一个更大、更单一的单一(微)服务,尤其是在 RAM 中保持模型的服务,可能会从 DDD 中受益。
首先,我复制粘贴了这篇文章 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; }
}
富域类的好处之一是您可以在每次引用任何层中的对象时调用它们的行为(方法)。此外,您倾向于编写一起协作的小型分布式方法。在贫血的领域类中,您倾向于编写通常由用例驱动的胖过程方法(在服务层中)。与富域类相比,它们通常不易维护。
具有行为的域类示例:
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()
selectedOrder
显示要由用户确认的项目列表,然后再单击保存按钮以保留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()
在奖金屏幕内打电话。
我认为问题的根源在于错误的二分法。如何提取这两个模型:丰富和“贫血”并将它们相互对比?我认为只有当你对什么是 class有错误的想法时才有可能。我不确定,但我想我是在 Youtube 的 Bozhidar Bozhanov 视频之一中找到的。类不是该数据的数据+方法。完全错误的理解导致将类分为两类:仅数据,因此贫乏模型和数据+方法-如此丰富的模型(更正确的是,还有第三类:甚至只有方法)。
真实的是,类是某个本体模型中的一个概念,一个词,一个定义,一个术语,一个想法,它是一个DENOTAT。而这种理解消除了错误的二分法:你不能只有贫血模型或只有丰富模型,因为这意味着你的模型是不充分的,它与现实无关:有些概念只有数据,有些只有方法,有些其中混合。因为在这种情况下,我们试图用类来描述一些类别、对象集、关系、概念,而众所周知,一些概念只是过程(方法),其中一些只是属性集(数据),一些它们是与属性的关系(混合)。
我认为一个适当的应用程序应该包括所有类型的类,并避免狂热地自我限制在一个模型上。不管逻辑是如何表示的:使用代码或可解释的数据对象(如Free Monads),无论如何:我们应该有表示过程、逻辑、关系、属性、特征、数据等的类(概念、表示),而不是尽量避免其中一些或将它们全部减少为一种。
因此,我们可以将逻辑提取到另一个类并将数据保留在原始类中,但这没有意义,因为某些概念可以包括属性和关系/过程/方法,并且将它们分开会在 2 个名称下重复该概念,这可以是简化为模式:“OBJECT-Attributes”和“OBJECT-Logic”。由于它们的局限性,在过程语言和函数语言中它很好,但对于一种允许您描述各种概念的语言来说,它是过度的自我约束。
贫血域模型对于 ORM 和通过网络轻松传输(所有商业应用程序的命脉)很重要,但 OO 对于封装和简化代码的“事务/处理”部分非常重要。
因此,重要的是能够识别并从一个世界转换到另一个世界。
将 Anemic 模型命名为 AnemicUser 或 UserDAO 等,以便开发人员知道可以使用更好的类,然后为非 Anemic 类提供适当的构造函数
User(AnemicUser au)
和适配器方法来创建用于传输/持久性的贫血类
User::ToAnemicUser()
旨在在运输/持久性之外的任何地方使用非贫血用户
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 所提供的一切,但有时你需要稍微扩展它以适应不断变化和更好的架构方法。
这是一个可能有帮助的例子:
贫血
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;
}
}