我决定是否应该使用富域模型而不是贫穷领域模型,并寻找两者的好例子。
我一直在使用由服务支持的Anemic Domain Model构建Web应用程序 - >存储库 - >存储层系统,使用FluentValidation进行BL验证,并将我的所有BL放入服务层。
我读过Eric Evan的DDD书,他(以及Fowler和其他人)似乎认为Anemic Domain Models是一种反模式。
所以我真的很想了解这个问题。
此外,我真的在寻找一个富域模型的好(基本)示例,以及它提供的贫血领域模型的好处。
答案 0 :(得分:46)
Bozhidar Bozhanov似乎在this博客文章中赞成贫血模型。
以下是他提出的摘要:
域对象不应该是spring(IoC)管理的,它们不应该有DAO或与注入其中的基础结构相关的任何东西
域对象具有依赖于hibernate(或持久性机制)设置的域对象
域对象执行业务逻辑,因为DDD的核心思想是,但这不包括数据库查询或CRUD - 仅对对象内部状态的操作
很少需要DTO - 在大多数情况下,域对象本身就是DTO(这节省了一些样板代码)
服务执行CRUD操作,发送电子邮件,协调域对象,基于多个域对象生成报告,执行查询等。
服务(应用程序)层不是那么薄,但不包括域对象固有的业务规则
应避免代码生成。应该使用抽象,设计模式和DI来克服代码生成的需要,最终 - 摆脱代码重复。
更新
我最近阅读了this文章,其中作者主张遵循一种混合方法 - 域对象可以仅根据其状态回答各种问题(在完全贫血模型的情况下,可能会在服务层)
答案 1 :(得分:43)
不同之处在于贫血模型将逻辑与数据分开。逻辑通常放在名为**Service
,**Util
,**Manager
,**Helper
的类中,依此类推。这些类实现数据解释逻辑,因此将数据模型作为参数。 E.g。
public BigDecimal calculateTotal(Order order){
...
}
而富域方法通过将数据解释逻辑放入富域模型来反转这一点。因此,它将逻辑和数据放在一起,富域模型看起来像这样:
order.getTotal();
这对对象一致性有很大影响。由于数据解释逻辑包装数据(数据只能通过对象方法访问),因此方法可以对其他数据的状态变化作出反应 - >这就是我们所说的行为。
在贫血模型中,数据模型不能保证它们处于合法状态,而在富域模型中它们可以。丰富的域模型应用OO原则,如封装,信息隐藏以及将数据和逻辑结合在一起,因此贫血模型是从OO角度反模式。
如需深入了解,请查看我的博客https://www.link-intersystems.com/blog/2011/10/01/anemic-vs-rich-domain-models/
答案 2 :(得分:35)
我的观点是:
贫血域模型=映射到对象的数据库表(仅字段值,无实际行为)
富域模型=公开行为的对象集合
如果你想创建一个简单的CRUD应用程序,也许带有经典MVC框架的贫血模型就足够了。但是如果你想实现某种逻辑,那么贫血模型就意味着你不会做面向对象的编程。
*请注意,对象行为与持久性无关。另一个层(Data Mappers,Repositories e.t.c。)负责持久化域对象。
答案 3 :(得分:11)
首先,我复制粘贴了本文的答案 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; }
}
答案 4 :(得分: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()
}
}
方法needToDeliver()
将返回需要交付的项目列表,包括奖金。它可以在类中,从另一个相关类或另一个层中调用。例如,如果您将Order
传递给视图,那么您可以使用所选needToDeliver()
的{{1}}来显示用户确认的项目列表,然后再单击“保存”按钮以保留{ {1}}。
回复评论
这是我使用控制器中的域类的方法:
Order
Order
及其def save = {
Order order = new Order()
order.addItem(new Item())
order.addItem(new Item())
repository.create(order)
}
的创建在一次交易中。如果无法创建其中一个Order
,则不会创建LineItem
。
我倾向于拥有代表单个交易的方法,例如:
LineItem
Order
内的任何内容都将作为一个单一交易执行。如果我需要在单个事务中执行许多不相关的方法,我会创建一个服务类。
为避免延迟加载异常,我使用JPA 2.1命名实体图。例如,在交付屏幕的控制器中,我可以创建加载def deliver = {
Order order = repository.findOrderByNumber('ORDER-1')
order.deliver()
// save order if necessary
}
属性的方法并忽略deliver()
,例如delivery
。在奖金屏幕中,我调用另一种加载bonus
属性并忽略repository.findOrderByNumberFetchDelivery()
的方法,例如bonus
。这需要dicipline,因为我仍然无法在奖励屏幕内拨打delivery
。
答案 5 :(得分:5)
当我过去编写整体桌面应用程序时,我构建了丰富的域模型,并乐于构建它们。
现在,我编写了微小的HTTP微服务,其中包括贫乏的DTO在内的代码尽可能少了。
我认为DDD和这种贫乏的争论可以追溯到整体桌面或服务器应用时代。我记得那个时代,我同意贫血模型是奇怪的。我建立了一个大型的整体外汇交易应用程序,但没有模型,真的,这太可怕了。
对于微服务而言,行为丰富的小型服务可以说是域内可组合的模型和集合。因此,微服务实现本身可能不需要进一步的DDD。微服务应用程序可能是域。
订单微服务可能只有很少的功能,以RESTful资源或通过SOAP或其他方式表示。订单微服务代码可能非常简单。
更大,更单一的单(微)服务,特别是将其保持在RAM中的模型,可能会受益于DDD。
答案 6 :(得分: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;
}
}
答案 7 :(得分:1)
我认为问题的根源在于错误的二分法。如何提取这两个模型:丰富模型和“贫血”模型并进行对比?我认为只有在您对什么是课程有错误的想法时,才有可能。我不确定,但是我想我是在Youtube上的Bozhidar Bozhanov视频之一中找到的。一个类不是数据+方法的数据。完全无效的理解导致将类分为两类:仅数据,因此贫血模型和数据+方法-如此丰富的模型(更准确地说,是第三类:仅方法)。
确实,类是某些本体模型中的一个概念,一个单词,一个定义,一个术语,一个想法,它是DENOTAT。这种理解消除了错误的二分法:您不能只有贫乏模型或富人模型,因为这意味着您的模型不够用,与现实不相关:某些概念仅包含数据,某些概念仅包含方法,有些他们混合在一起。因为在这种情况下,我们尝试描述一些类别,对象集,关系,带有类的概念,并且我们知道,一些概念仅是过程(方法),一些仅是属性集(数据),一些它们是与属性的关系(混合)。
我认为适当的应用程序应包括所有种类的类,并避免狂热地自我限制为仅一个模型。无论如何,逻辑是如何表示的:无论如何使用代码或可解释的数据对象(如Free Monads):我们应该具有表示流程,逻辑,关系,属性,特征,数据等的类(概念,符号)而不是要避免它们中的某些或仅将它们减少为一种。
因此,我们可以将逻辑提取到另一类并将数据保留在原始类中,但是这是没有意义的,因为某些概念可以包含属性和关系/过程/方法,并且将它们分开将在2个名称下重复该概念可以简化为模式:“对象属性”和“对象逻辑”。在程序和功能语言中,由于它们的局限性,所以很好,但是对于一种允许您描述各种概念的语言来说,它过于自我约束。
答案 8 :(得分:0)
贫血领域模型对于ORM和通过网络轻松传输非常重要(所有商业应用程序的生命线),但OO对于封装和简化代码的“事务/处理”部分非常重要。
因此,重要的是能够识别并从一个世界转换到另一个世界。
名称Anemic模型,如AnemicUser或UserDAO等,因此开发人员知道有一个更好的类可供使用,然后为无Anemic类提供适当的构造函数
User(AnemicUser au)
和适配器方法来创建用于传输/持久化的贫血等级
User::ToAnemicUser()
旨在在传输/持久性之外的任何地方使用无贫血用户
答案 9 :(得分:-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所提供的所有功能,但是有时您需要对其进行扩展以适应不断变化的更好的体系结构。