23

如果这是重复的,我深表歉意,但我在相关问题中找不到关于该主题的任何具体示例。

在阅读了 Martin Fowler 关于“贫血域模型”的文章后,我对为什么这被认为是一种反模式感到困惑。甚至大多数企业开发人员是否认为它是一种反模式,因为 AFAIK 可能 90% 的 j2ee 应用程序都是以“贫血”的方式设计的?

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

谢谢,

4

6 回答 6

62

Martin Fowler 给这个行业带来了很多话,却很少了解。

今天的大多数应用程序(web/db)确实需要许多公开其属性的对象。

任何反对这种做法的权威(自称)都应该以身作则,向我们展示一个成功的现实世界应用程序,其中充满了他奇妙原理的体现。

否则就闭嘴。令人作呕的是,我们的行业有这么多热空气。这是工程,不是戏剧俱乐部。

于 2011-06-09T15:24:24.263 回答
59

如需完整答案,请查看我的博客,其中还包含源代码示例 [博客]: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 添加到 Order 中Order.getTotal(),只要 OrderService 没有重新计算该值,该值就是不正确的。在现实世界的应用程序中,找出谁添加了订单项以及为什么没有调用 OrderService 可能很麻烦。正如您可能已经认识到的那样,Order 还破坏了订单项列表的封装。有人可以打电话order.getItems().add(orderItem)来添加订单项目。这会使得很难找到真正添加项目的代码(order.getItems()引用可以通过整个应用程序传递)。
  • '方法负责计算所有 Order 对象的总数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;
    }
}

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

参考

于 2013-02-22T09:11:45.270 回答
17

好。你是对的,几乎所有的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++ 出现时,struct 变成了一个类,它允许您将函数放入 struct(类)中。然后该结构作为 this 指针隐式传递,因此您无需创建结构并将其传递给函数,而是创建类并针对它调用方法。这样代码更清晰,更容易理解。

然后我转到java世界,大家把模型和服务分开,也就是说模型是一个美化的struct,而服务本来就是无状态的,变成了对模型进行操作的函数集合。对我来说,这听起来很像 ac 语言习语。这很有趣,因为在 c 中完成它是因为该语言没有提供更好的东西,而在 java 中它完成是因为程序员不知道更好。

于 2011-06-09T14:22:36.040 回答
12

给定以下两个类:

class CalculatorBean  
{  
    //getters and setters  
}  

class CalculatorBeanService  
{  
   Number calculate(Number first, Number second);  
    {  
       //do calculation  
    }  
} 

如果我理解正确,Fowler 是说因为你CalculatorBean只是一堆 getter/setter,所以你没有从中获得任何真正的价值,如果你将该对象移植到另一个系统,它将什么也不做。问题似乎您CalculatorBeanService包含CalculatorBean应该负责的所有内容。这不是最好的,因为现在将CalculatorBean其所有责任都委托给了CalculatorBeanService

于 2011-06-09T14:10:04.173 回答
5

与软件开发世界中的大多数事情一样,没有黑白之分。在某些情况下,贫血的域模型非常适合。

但是在很多情况下,开发人员尝试构建域模型,也就是做 DDD,并最终以贫乏的域模式结束。我认为在这种情况下,贫血域模型被认为是一种反模式。

只要确保你使用最好的工具来完成这项工作,如果它对你有用,就不要费心去改变它。

于 2011-06-10T07:51:23.273 回答
5
于 2017-01-16T22:57:18.960 回答