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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/6293981/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-30 15:14:40  来源:igfitidea点击:

Concrete examples on why the 'Anemic Domain Model' is considered an anti-pattern

javaarchitecturedomain-driven-designanti-patternsanemic-domain-model

提问by Simeon

I apologize if this is a duplicate, but I couldn't find any concrete examples on the topic in related questions.

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

After reading Martin Fowler's article on the 'Anemic Domain Model', I'm left wandering as to why is this considered an anti-pattern. Even does the majority of enterprise developers consider it an anti-pattern, since AFAIK probably 90% of the j2ee applications are designed in an 'anemic' way ?

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

Can someone recommend further reading on the topic (other than the 'Domain Driven Design' book), or even better, give a concrete examples on how this anti-pattern is affecting application design in a bad way.

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

Thanks,

谢谢,

采纳答案by Woot4Moo

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 CalculatorBeanis 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 CalculatorBeanServicecontains everything that the CalculatorBeanshould be responsible for. Which is not the best as now the CalculatorBeandelegates all of its responsibility to the CalculatorBeanService

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

回答by René Link

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/

有关完整答案,请查看我的博客,其中还包含源代码示例 [博客]: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:

那么解释订单和订单项目以计算订单总额的逻辑在哪里?此逻辑通常放置在名为 *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);
    }
}

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 Orderdoesn't have a property total. If you now make the Orderimmutable you are on the way to functional programming. But this is another topic that I can't discover here.

在这种情况下,Order没有 property total。如果您现在使Order不可变,您就在进行函数式编程。但这是我无法在这里发现的另一个主题。

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.
  • 如果有人将 OrderItem 添加到 Order,Order.getTotal()则只要 OrderService 没有重新计算该值,该值就是不正确的。在现实世界的应用程序中,找出谁添加了订单项以及为什么没有调用 OrderService 可能很麻烦。正如您可能已经认识到的那样,订单还会破坏订单项目列表的封装。有人可以打电话order.getItems().add(orderItem)添加订单项目。这会使找到真正添加项目的代码变得困难(order.getItems()引用可以通过整个应用程序传递)。
  • OrderServicecalculateTotal方法负责计算总所有Order对象。因此它必须是无状态的。但是无状态也意味着它不能缓存总值,只有在 Order 对象改变时才重新计算它。因此,如果calculateTotal 方法需要很长时间,您也会遇到性能问题。尽管如此,您还是会遇到性能问题,因为客户可能不知道订单是否处于合法状态,因此calculateTotal(..)即使在不需要时也会预防性地调用。

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 totalstate, because it won't have a totalproperty. But this also means that the totalmust be calculated every time it is needed. By removing the totalproperty 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 totalvalue 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).

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

参考

回答by irreputable

Martin Fowler brings this industry many words and less understanding.

Martin Fowler 给这个行业带来了很多话和更少的理解。

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

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

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.

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

回答by Kevin

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:

好。你说得对,几乎所有的 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
}

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.

也就是说语言不允许我们在结构体内部组合函数对 SomeStruct 进行操作,所以我们创建了一组自由函数,按照约定将 SomeStruct 作为第一个参数。

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.

当 c++ 出现时,结构体变成了一个类,它允许你将函数放入结构体(类)中。然后该结构作为 this 指针隐式传递,因此不是创建结构并将其传递给函数,而是创建类并针对它调用方法。这样代码更清晰,更容易理解。

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.

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

回答by Iulian Margarintescu

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.

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

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

只要确保您使用最适合这项工作的工具,如果它适用,您就不必费心改变它。

回答by GorkemHalulu

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.

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