“贫血领域模型”为什么被认为是反模式的具体例子

27

对于这个主题我如果提供的内容重复了,那么请见谅。在相关问题中,我找不到任何关于这个主题的具体例子。

在阅读马丁·福勒(Martin Fowler)的有关“贫血对象模型”文章之后,我不明白为什么它被认为是一种反模式。因为据我所知,也许90%的j2ee应用程序都是以“贫血”的方式设计的,即使大多数企业开发人员认为它是一种反模式?

是否有人可以推荐更多有关此主题的阅读材料(除了“领域驱动设计”书籍),或者更好的是,给出关于如何以不良方式影响应用程序设计的具体示例来说明这种反模式。

谢谢


3
我看到一位 Stack Overflow 用户的非常有趣的帖子,值得一读:http://techblog.bozho.net/?p=180。 - planetjones
@planetjones 我已经阅读了那篇文章,确实很有趣,这也是我提出这个问题的原因之一。谢谢。 - Simeon
6个回答

69

马丁·福勒给这个行业带来了许多词汇,但理解却很少。

现今大部分应用程序(Web/数据库)确实需要许多对象来公开它们的属性。

任何自称权威的人士对此做法不以为然时,都应该以身作则,并向我们展示一个成功的真实应用程序,其中充满了他神奇原则的具体体现。

否则就请闭嘴。我们这个行业有太多的空话了。这是工程领域,不是戏剧俱乐部。


18
如果可以的话,我会给这个+1000票赞。这正是我的观点。如果你批评什么,请举例说明 :) 这也正是我提出这个问题的原因。 - Simeon
15
这可能应该是一条评论。 - Woot4Moo
7
@Simeon,我只是在评论中指出回答最好作为评论,因为它除了某人的观点之外没有为讨论增添任何内容。 - Woot4Moo
7
翻译:哇!这是我喜欢的回答。我花了很多时间研究所谓的全面性建筑概念。潜在的好处通常是显而易见的。我真正希望了解的是“有哪些缺点”?有了这些信息,我就可以权衡是否将此解决方案用于我的情况。在这种情况下,“无血统领域模型”只是针对一种有效但应考虑其缺点的选择所做的贬义描述。 - Johnny Kauffman
5
我希望我能给这个回答点赞两次。 - talles
显示剩余6条评论

64

要获取完整答案,请查看我的博客,其中还包含源代码示例 [blog]: https://www.link-intersystems.com/blog/2011/10/01/anemic-vs-rich-domain-models/

从面向对象的角度来看,贫血领域模型绝对是反模式,因为它是纯过程化编程。 之所以称其为反模式,是因为贫血领域模型没有涵盖面向对象的主要原则:

面向对象意味着:一个对象管理其状态并确保它在任何时候都处于合法状态。(数据隐藏、封装)

因此,一个对象封装数据并管理其访问和解释。 相比之下,贫血模型不能保证在任何时候都处于合法状态。

订单及订单项的示例将有助于展示差异。 因此,让我们来看一下订单的贫血模型。

一个贫血模型

 public class Order {
    private BigDecimal total = BigDecimal.ZERO;
    private List<OrderItem> items = new ArrayList<OrderItem>();

    public BigDecimal getTotal() {
        return total;
    }

    public void setTotal(BigDecimal total) {
        this.total = total;
    }

    public List<OrderItem> getItems() {
        return items;
    }

    public void setItems(List<OrderItem> items) {
        this.items = items;
    }
}

public class OrderItem {

    private BigDecimal price = BigDecimal.ZERO;
    private int quantity;
    private String name;
    
    public BigDecimal getPrice() {
        return price;
    }

    public void setPrice(BigDecimal price) {
        this.price = price;
    }

    public int getQuantity() {
        return quantity;
    }

    public void setQuantity(int quantity) {
        this.quantity = quantity;
    }
}

那么,解释订单和订单项目以计算订单总额的逻辑位于何处? 这种逻辑通常放置在名为*Helper、*Util、*Manager或简单地*Service的类中。 在贫血模型中,订单服务将如下所示:

public class OrderService {
    public void calculateTotal(Order order) {
        if (order == null) {
             throw new IllegalArgumentException("order must not be null");
        }

        BigDecimal total = BigDecimal.ZERO;
        List<OrderItem> items = order.getItems();

        for (OrderItem orderItem : items) {
            int quantity = orderItem.getQuantity();
            BigDecimal price = orderItem.getPrice();
            BigDecimal itemTotal = price.multiply(new BigDecimal(quantity));
            total = total.add(itemTotal);
        }
        order.setTotal(total);
    }
}

在贫血模型中,您调用一个方法并将贫血模型传递给它,以使贫血模型达到合法状态。因此,贫血模型的状态管理被放置在贫血模型之外,从面向对象的角度来看,这是一种反模式。
有时,您会看到略有不同的服务实现方式,它不修改贫血模型,而是返回它计算出的值。例如:
public BigDecimal calculateTotal(Order order); 

在这种情况下,Order没有一个total属性。如果现在使Order不可变,则可以朝着函数式编程的方向发展。但这是另一个话题,我无法在此探讨。
上述贫血订单模型存在以下问题:
  • 如果有人向订单添加OrderItem,则只要OrderService没有重新计算, Order.getTotal()值就是不正确的。在实际应用中,找出谁添加了订单项以及为什么没有调用OrderService可能会很麻烦。正如您可能已经意识到的那样,Order还破坏了订单项列表的封装性。有人可以调用order.getItems().add(orderItem)来添加订单项。这可能会使查找真正添加该项的代码变得困难(order.getItems()引用可以通过整个应用程序传递)。
  • OrderServicecalculateTotal方法负责计算所有订单对象的总数。因此,它必须是无状态的。但是,无状态也意味着它不能缓存总值,并且仅在Order对象更改时重新计算它。因此,如果calculateTotal方法需要很长时间,您也会遇到性能问题。尽管如此,您仍然会遇到性能问题,因为客户端可能不知道订单是否处于合法状态,因此即使不需要,也会预防性地调用calculateTotal(..)

有时您还会看到服务不会更新贫血模型,而只是返回结果。例如:

public class OrderService {
    public BigDecimal calculateTotal(Order order) {
        if (order == null) {
             throw new IllegalArgumentException("order must not be null");
        }

        BigDecimal total = BigDecimal.ZERO;
        List<OrderItem> items = order.getItems();

        for (OrderItem orderItem : items) {
            int quantity = orderItem.getQuantity();
            BigDecimal price = orderItem.getPrice();
            BigDecimal itemTotal = price.multiply(new BigDecimal(quantity));
            total = total.add(itemTotal);
        }
       return total;
    }
}

在这种情况下,服务会在某个时间解释贫血模型的状态,并不使用结果更新贫血模型。这种方法的唯一好处是,贫血模型不会包含无效的total状态,因为它将没有total属性。但这也意味着每次需要时必须重新计算total。通过移除total属性,您可以引导开发人员使用服务,而不是依赖于total的属性状态。但这并不能保证开发人员以某种方式缓存total值,因此他们可能也会使用过时的值。这种实现服务的方式可以在一个属性是从另一个属性派生出来时使用。或者换句话说...当您解释基本数据时。例如:int getAge(Date birthday)

现在看看丰富的领域模型,看看它们之间的区别。

丰富的领域模型方法

public class Order {

    private BigDecimal total;
    private List<OrderItem> items = new ArrayList<OrderItem>();

    /**
      * The total is defined as the sum of all {@link OrderItem#getTotal()}.
      *
      * @return the total of this {@link Order}.
      */
    public BigDecimal getTotal() {
        if (total == null) {
           /*
            * we have to calculate the total and remember the result
            */
           BigDecimal orderItemTotal = BigDecimal.ZERO;
           List<OrderItem> items = getItems();

           for (OrderItem orderItem : items) {
               BigDecimal itemTotal = orderItem.getTotal();
               /*
                * add the total of an OrderItem to our total.
                */
               orderItemTotal = orderItemTotal.add(itemTotal);
           }

           this.total = orderItemTotal;
           }
        return total;
        }

   /**
    * Adds the {@link OrderItem} to this {@link Order}.
    *
    * @param orderItem
    *            the {@link OrderItem} to add. Must not be null.
    */
    public void addItem(OrderItem orderItem) {
        if (orderItem == null) {
            throw new IllegalArgumentException("orderItem must not be null");
        }
        if (this.items.add(orderItem)) {
           /*
            * the list of order items changed so we reset the total field to
            * let getTotal re-calculate the total.
            */ 
            this.total = null;
        }
    }

    /**
      *
      * @return the {@link OrderItem} that belong to this {@link Order}. Clients
      *         may not modify the returned {@link List}. Use
      *         {@link #addItem(OrderItem)} instead.
      */
    public List<OrderItem> getItems() {
       /*
        * we wrap our items to prevent clients from manipulating our internal
        * state.
        */
        return Collections.unmodifiableList(items);
    }

}

public class OrderItem {

    private BigDecimal price;

    private int quantity;

    private String name = "no name";

    public OrderItem(BigDecimal price, int quantity, String name) {
     if (price == null) {
      throw new IllegalArgumentException("price must not be null");
     }
     if (name == null) {
      throw new IllegalArgumentException("name must not be null");
     }
     if (price.compareTo(BigDecimal.ZERO) < 0) {
      throw new IllegalArgumentException(
        "price must be a positive big decimal");
     }
     if (quantity < 1) {
      throw new IllegalArgumentException("quantity must be 1 or greater");
     }
     this.price = price;
     this.quantity = quantity;
     this.name = name;
    }

    public BigDecimal getPrice() {
     return price;
    }

    public int getQuantity() {
     return quantity;
    }

    public String getName() {
     return name;
    }

    /**
      * The total is defined as the {@link #getPrice()} multiplied with the
      * {@link #getQuantity()}.
      *
      * @return
      */
    public BigDecimal getTotal() {
     int quantity = getQuantity();
      BigDecimal price = getPrice();
      BigDecimal total = price.multiply(new BigDecimal(quantity));
     return total;
    }
}

丰富的领域模型尊重面向对象的原则,并保证其在任何时候都处于合法状态。

参考资料


2
如果出现贫血模型,你不能通过OrderService来强制添加OrderItem吗? - hjdm
如果您想强制执行它,首先必须将“Order”的“setItems”方法设置为包范围。因此,您必须将其放置在与“OrderService”相同的包中,以便“OrderService”仍然可以修改“Order”。尽管如此,同一包中的所有其他类也可以修改“Order”。因此,您只是减少了问题区域。在这种情况下,您尝试通过约定来强制执行对象的合法状态:“仅通过OrderService修改订单”。但是,您如何确保现在同一包中的其他类访问它并将其带入非法状态? - René Link
1
你说贫血领域模型是过程式的,但它也可以用函数式完成,不是吗? - Didier A.
8
我认为这应该标记为正确答案。 - pigiuz
所谓贫血领域模型就是带有数据结构的过程式编程,而所谓丰富领域模型则是面向对象编程。无论哪种方式,都可以根据《Clean Code》的原则进行工作。如果您想使用领域对象(而不是领域数据结构),那么您当然需要使用丰富的领域模型。 - inf3rno
显示剩余2条评论

18

嗯,你说得对,几乎所有的Java代码都是这样写的。这种写法被认为是反模式的原因是面向对象设计的一个主要原则是将数据和操作数据的函数组合成一个单一的对象。例如,当我编写老式的C语言代码时,我们会像这样模拟面向对象设计:

struct SomeStruct {
    int x;
    float y;
};

void some_op_i(SomeStruct* s, int x) {
    // do something
}
void some_op_f(SomeStruct* s, float y) {
    // something else
}

换句话说,该语言不允许我们在结构体内部组合操作SomeStruct的函数,因此我们创建了一组自由函数,按照惯例将SomeStruct作为第一个参数。

当C++出现时,结构体变成了类,并允许您将函数放入结构体(类)中。然后,结构体被隐式传递为this指针,因此您可以创建类并调用其方法,而不是创建结构体并将其传递给函数。这样编写的代码更清晰、更易于理解。

后来我转向了Java领域,人们将模型与服务分开,即模型是一种较高级别的结构体,而服务则是一组在模型上操作的函数。对我来说,这听起来很像一种C语言习惯用法。这很有趣,因为在C语言中这样做是因为该语言没有提供更好的替代方案,在Java中这样做是因为程序员不知道有更好的方案。


有趣的比较 :) 我会仔细考虑一下。 - Simeon
1
我认为这不是一个准确的答案。在面向对象的代码中,你也可以有数据结构。这没有任何问题,例如像领域事件或命令一样的DTO也是数据结构。按照定义,像实体、值对象等领域对象都具有行为,因此它们不应该是数据结构...但我仍在寻找那个定义:D - inf3rno
你正在混淆面向对象的原则。我可以拥有一个完全遵循继承、多态等面向对象特性的领域服务对象。方法可以接收实体模型并像处理普通数据对象一样进行操作。例如,我有一个领域服务,用于为给定的人员实体获取护照。我所需的只是来自人员实体的信息(getter),人员实体是否负责了解创建护照的行政政府流程?不需要。另一个政府服务,也就是另一个领域服务需要处理此事。还有一个名为“模型验证器”的新领域服务。 - code5

14

考虑以下两个类:

class CalculatorBean  
{  
    //getters and setters  
}  

class CalculatorBeanService  
{  
   Number calculate(Number first, Number second);  
    {  
       //do calculation  
    }  
} 
如果我理解正确的话,Fowler 的意思是因为你的 CalculatorBean 只是一堆 getter/setter,所以你不会从中获得任何真正的价值,如果你将该对象移植到另一个系统中,它将没有作用。问题似乎在于你的 CalculatorBeanService 包含了 CalculatorBean 应该负责的所有内容。这并不是最好的方案,因为现在 CalculatorBean 将所有的职责委托给了 CalculatorBeanService

8
不完全是这样。关注点分离意味着一个对象只负责一件事情。模型仍应该以这种方式设计。只是“贫血领域模型”根本不负责任何事情。 - Kevin
3
@Simeon: 显然,这其中涉及到某些判断和艺术性的因素。最终目的是要编写容易阅读和维护的代码。举个反例:我有一个名为“Job”的模型对象,它恰好被持久化到某个存储器中。当我从存储器中获取它时,应该能够运行它。这应该放在一个服务对象中呢,还是直接在job对象中暴露一个run()方法?我认为直接用job.run()更自然,比使用jobRunnerService.runJob(job)更好。 - Kevin
3
你的意思是“数据库代码”指的是DAO(数据访问对象)或存储过程吗?我认为在这两种情况下都不同意。也许是因为我的当前思维模式,但生成新用户名的逻辑与任何其他因素无关。而判断用户名是否已被使用的逻辑则依赖于数据库。因此,我目前的“感觉”是生成用户名的逻辑应该在服务中,而不是在领域对象中。 - Simeon
1
@Simeon 在大O标记中,最坏情况才是重要的,而不是最好情况。虽然它不同于模式本身,但它确实展示了在一处良好的东西在另一处却是糟糕的情况。 - Woot4Moo
1
@Simeon:我认为(当然只是我的个人意见),Fowler所反对的是人们普遍采用“贫血领域模型”模式,无论是因为他们不知道更好的方法,还是作为不思考良好设计的借口。 - Kevin
显示剩余20条评论

7

这简单违反了“告诉,别问”原则,即对象应该告诉客户端它们可以或不能做什么,而不是暴露属性并让客户端确定对象是否处于特定状态以执行某个操作。


6

和软件开发领域的大多数事情一样,没有黑白之分。有些情况下贫血领域模型是最合适的选择。

但是有很多情况下,开发人员试图构建一个领域模型,也就是进行DDD,结果却得到了一个贫血领域模型。我认为在这种情况下,贫血领域模型被认为是反模式。

只要确保使用最适合工作的工具,如果它对你有效,就不要费心去改变它。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接