处理贫血域模型的技术

时间:2009-03-04 07:00:58

标签: domain-driven-design business-logic

我已经阅读了一些关于贫血领域模型和关注点分离的问题。在贫血域对象上执行/附加域逻辑的最佳技术是什么?在我的工作中,我们有一个非常贫穷的模型,我们目前正在使用“帮助程序”类来执行域对象上的数据库/业务逻辑。例如:

public class Customer
{
    public string Name {get;set;}
    public string Address {get;set;}
}

public class Product
{
    public string Name {get;set;}
    public decimal Price {get;set;}
}

public class StoreHelper
{
    public void PurchaseProduct(Customer c, Product p)
    {
         // Lookup Customer and Product in db
         // Create records for purchase
         // etc.
    }
}

当应用需要购买时,它会创建StoreHelper,并在域对象上调用该方法。对我而言,客户/产品知道如何将自己保存到存储库是有意义的,但您可能不希望在域对象上使用Save()方法。对于像Customer.Purchase(Product)这样的方法也有意义,但这就是将域逻辑放在实体上。

以下是我遇到的一些技巧,不确定哪些是好/坏:

  1. 客户和产品继承自“实体”类,该类以通用方式提供基本的CRUD操作(可能使用ORM)。
    • 优点:每个数据对象都会自动获取CRUD操作,但随后绑定到数据库/ ORM
    • 缺点:这不能解决对象上的业务操作问题,也会将所有域对象绑定到可能不合适的基本实体
  2. 使用帮助程序类来处理CRUD操作和业务逻辑
    • 为“纯数据库”操作设置DAO是否有意义,并为更具体业务的操作分离业务助手?
    • 为此使用非静态或静态辅助类更好吗?
    • 优点:域对象不依赖于任何数据库/业务逻辑(完全贫血)
    • 缺点:不是很OO,在应用程序代码中使用帮助器并不是很自然(看起来像C代码)
  3. 使用Double Dispatch技术,其中实体具有保存到任意存储库的方法
    • 优点:更好地分离关注点
    • 缺点:实体附加了一些额外的逻辑(虽然它已经解耦)
  4. 在C#3.0中,您可以使用扩展方法将CRUD /业务方法附加到域对象而不触及它
    • 这是一种有效的方法吗?什么是利弊?
  5. 其他技巧?
  6. 处理此问题的最佳技巧是什么?我对DDD很新(我正在阅读埃文斯的书 - 所以也许这会打开我的眼睛)

4 个答案:

答案 0 :(得分:15)

为了避免贫血模型,重构你的助手类:

逻辑如:
“Customer.PurchaseProduct(产品,付款支付)”,
“Customer.KillCustomer(人员杀手,武器武器)”
应存在于“客户”域对象中。

逻辑如:
“Customer.IsCustomerAlive()”
“Customer.IsCustomerHappy()”
应该遵守规范。

逻辑如:
“Customer.Create()”,
“Customer.Update()”
显然应该去存储库。

逻辑如:
“Customer.SerializeInXml()”
“Customer.GetSerializedCustomerSizeInBytes()”
应该去服务。

复杂的构造函数应该去工厂。

这就是我的看法。如果有人能评论我对DDD方法的理解,我会很高兴。


修改

有时 - 贫血领域模型shouldn't be avoided

编辑我的回答补充说DDD不是关于拾取和丢弃模式 DDD是我们思考的方式。

答案 1 :(得分:7)

Martin Fowler撰写了很多关于域模型的文章,包括anemic domain models。他还为可能有用的域模型和数据库提供了许多设计模式的简要描述(和UML类图):Catalog of "Patterns of Enterprise Application Architecture"

我建议查看Active RecordData Mapper模式。从您的问题描述中,听起来您的帮助程序类包含域/业务规则数据库实现细节。

Active Record会将帮助程序的域逻辑和数据库代码移动到其他域对象(例如您的Entity基类)。 Data Mapper会将帮助程序的域逻辑移动到域对象中,将数据库代码移动到单独的映射对象中。这两种方法都比面向对象的助手类更加面向对象。

Eric Evans的“领域驱动设计”一书非常出色。它有点干,但绝对值得。 InfoQ有一个"Domain Driven Design Quickly" mini-book,这是对埃文斯的书的一个很好的介绍。加上“快速领域驱动设计”可以免费PDF格式获得。

答案 2 :(得分:2)

我一直认为贫血领域模型是一种反模式。很明显,客户会购买产品,这种能力可以通过接口实现来实现

Interface IPurchase
      Purchase(Product);

,因此您的任何域对象都可以根据需要实现。通过这种方式,您可以为域对象引入功能 - 这正是它应该的位置。

答案 3 :(得分:0)

您未提及的一种方法是使用AOP来处理您的数据访问。我最近使用这种方法的一个例子(尽管为了发布目的而大大简化)是我有一个Account域实体,它有一个debit方法,封装了所需的业务逻辑以便成功从帐户中扣款。

N.B。所有代码都是带有AspectJ AOP表示法的Java ...

public boolean debit(int amount) {
    if (balance - amount >= 0) {
        balance = balance - amount;
        return true;
    }
    return false;
}

将适当的存储库注入到我的方面,然后使用切入点截取对此方法的调用...

pointcut debit(Account account,int amount) :
    execution(boolean Account.debit(int)) &&
    args(amount) &&
    target(account);

...并应用了一些建议:

after(Account account, int amount) returning (boolean result)  : debit(account,amount) {
    if (result) getAccountRepository().debit(account, amount);
}

在我看来,这可以很好地区分问题,并允许您的域实体完全专注于应用程序的业务逻辑。