具体例子说明为什么“贫血领域模型”如此重要被认为是反模式

发布于 2024-11-14 16:50:59 字数 1436 浏览 4 评论 0原文

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

给妤﹃绝世温柔 2024-11-21 16:50:59

Martin Fowler 给这个行业带来的说法很多,但理解却很少。

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

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

不然就闭嘴。令人作呕的是,我们这个行业存在如此多的热议。这是工程,不是戏剧俱乐部。

Martin Fowler brings this industry many words and less understanding.

Majority of applications today (web/db) do need many objects that expose their properties.

Any authority (self claimed) frowning upon such practice should lead by example, and show us a successful real world application that's full of embodiments of his marvelous principles.

Or else shut up. It is sickening that there so many hot airs in our industry. This is engineering, not a drama club.

你的往事 2024-11-21 16:50: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 中,只要 OrderService 没有重新计算,Order.getTotal() 值就是不正确的。在现实世界的应用程序中,找出谁添加了订单项以及为什么 OrderService 未被调用可能很麻烦。正如您可能已经认识到的那样,订单还打破了订单项目列表的封装。有人可以调用 order.getItems().add(orderItem) 来添加订单项。这可能会导致很难找到真正添加项目的代码(order.getItems() 引用可以通过整个应用程序传递)。
  • OrderServicecalculateTotal 方法负责计算所有 Order 对象的总计。因此它必须是无状态的。但无状态也意味着它不能缓存总值,只有在 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(日期生日)

现在看一下富域模型,看看其中的差异。

富域方法

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;
    }
}

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

参考

For the complete answer take a look at my blog that also contains source code examples [blog]: https://www.link-intersystems.com/blog/2011/10/01/anemic-vs-rich-domain-models/

If you look at the anemic domain model from an object oriented perspective it is definitely an anti-pattern because it is pure procedural programming.
The reason why it is called an anti-pattern is that the main object oriented principle is not covered by an anemic domain model:

Object oriented means that: an object manages its state and guarantees that it is in a legal state at any time. (data hiding, encapsulation)

Therefore an object encapsulates data and manages the access and interpretation of it.
In contrast to this an anemic model does not gurantee that it is in a legal state at any time.

An example of an order with order items will help to show the difference.
So let's take a look at an anemic model of an order.

An anemic model

 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;
    }
}

So where is the logic located that interprets the order and order items to calculate an order total?
This logic is often placed in classes named *Helper, *Util, *Manager or simply *Service.
An order service in an anemic model would look like this:

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);
    }
}

In an anemic model you invoke a method and pass it the anemic model to bring the anemic model to a legal state. Therefore the anemic model's state management is placed outside the anemic model and this fact makes it an anti-pattern from an object oriented perspective.

Sometimes you will see a slightly different service implementation that does not modify the anemic model. Instead it returns the value it calculates. E.g.

public BigDecimal calculateTotal(Order order); 

In this case the Order doesn't have a property total. If you now make the Order immutable you are on the way to functional programming. But this is another topic that I can't discover here.

The problems with the anemic order model above are:

  • If someone adds an OrderItem to the Order the Order.getTotal() value is incorrect as long as it has not been recalculated by the OrderService. In a real world application it can be cumbersome to find out who added the order item and why the OrderService has not been called. As you might have recognized already the Order also breaks encapsulation of the order items list. Someone can call order.getItems().add(orderItem) to add an order item. That can make it difficult to find the code that really adds the item (order.getItems() reference can be passed through the whole application).
  • The OrderService's calculateTotalmethod is responsible for calculating the total for all Order objects. Therefore it must be stateless. But stateless also means that it can not cache the total value and only recalculate it if the Order object changed. So if the calculateTotal method takes a long time you also have a performance issue. Nevertheless you will have performance issues, because clients might not know if the Order is in a legal state or not and therefore preventatively call calculateTotal(..) even when it is not needed.

You will also see sometimes that services do not update the anemic model and instead just return the result. E.g.

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;
    }
}

In this cases the services interpret the state of the anemic model at some time and do not update the anemic model with the result. The only benefit of this approach is that the anemic model can not contain an invalid total state, because it won't have a total property. But this also means that the total must be calculated every time it is needed. By removing the total property you lead developers to use the service and to not to rely on the total's property state. But this will not guarantee that the developers cache the total value in some way and thus they might also use values that are outdated. This way of implementing a service can be done whenever a property is derived form another property. Or in other words... when you interpret basic data. E.g. int getAge(Date birthday).

Now take a look at the rich domain model to see the difference.

The rich domain approach

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;
    }
}

The rich domain model respects the object oriented principles and gurantees that it is in a legal state at any time.

References

故人爱我别走 2024-11-21 16:50:59

出色地。你是对的,几乎所有的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世界,大家把模型和服务分开了,也就是说模型是一个美化的结构体,而服务本身是无状态的,变成了操作模型的函数集合。对我来说,这听起来很像 ac 语言习语。这很有趣,因为在 c 中这样做是因为该语言没有提供更好的东西,而在 java 中这样做是因为程序员不知道更好的东西。

Well. You're right that almost all java code is written this way. The reason it's an anti pattern is that one of the main principles of object oriented design is to combine data and the functions that operate on it into a single object. For example when I was writing old school c code, we would mimic object oriented design like this:

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
}

Which is to say that the language didn't allow us to combine the functions to operate on SomeStruct inside of the struct, so we created a group of free functions that by convention took SomeStruct as a first param.

When c++ came along, the struct became a class, and it allows you to put functions into the struct (class). Then the struct is implicitly passed as the this pointer, so instead of creating a struct and passing it to functions, you create the class and call methods against it. The code is more clear and easier to understand this way.

Then I moved to the java world, and everyone separates the model from the service, which is to say the model is a glorified struct, and the service, being stateless as it is, becomes a collection of functions that operates on a model. Which to me, sounds suspiciously like a c language idiom. It's pretty funny because in c it was done because the language didn't offer anything better, and in java it's done because the programmers don't know any better.

笙痞 2024-11-21 16:50:59

给定以下两个类:

class CalculatorBean  
{  
    //getters and setters  
}  

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

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

Given the following two classes:

class CalculatorBean  
{  
    //getters and setters  
}  

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

If I understand correctly, Fowler is stating that because your CalculatorBean is just a bunch of getters/setters you don't gain any real value from it and if you port that object to another system it will do nothing. The problem seems that your CalculatorBeanService contains everything that the CalculatorBean should be responsible for. Which is not the best as now the CalculatorBean delegates all of its responsibility to the CalculatorBeanService

七分※倦醒 2024-11-21 16:50:59

它只是违反了“告诉,不要问”原则,该原则规定对象应该告诉客户端它们可以做什么或不能做什么,而不是公开属性并让客户端来确定对象是否处于特定状态以进行给定的操作。

It simply violates the “Tell, Don’t Ask” principle which states that objects should tell the client what they can or cannot do rather than exposing properties and leaving it up to the client to determine if an object is in a particular state for a given action to take place.

冰雪梦之恋 2024-11-21 16:50:59

与软件开发世界中的大多数事情一样,没有黑白分明。在某些情况下,贫血域模型是完美的选择。

但在很多情况下,开发人员尝试构建域模型(又名 DDD),但最终却陷入贫乏的域模式。我认为在这种情况下,贫血域模型被认为是一种反模式。

只要确保您使用最适合您的工作的工具即可,如果它适合您,则无需更换它。

As with most things in the software development world there is not black and white. There are cases where an anemic domain model is the perfect fit.

BUT there are a lot of cases where developers try to build a domain model, aka do DDD, and end up with an anemic domain mode instead. I think in this case the anemic domain model is considered an anti-patern.

Just make sure you use the best tool for the job and if it works for you don't bother changing it.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文