开发者

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

Closed. This question needs to be more focused. It is not currently accepting answers.

Want to improve this question? Update the question so it focuses on one problem only by editing this post.

开发者_运维百科

Closed 5 years ago.

Improve this question

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 ?

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,


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.


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 calculateTotal method 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

  • Blog: https://www.link-intersystems.com/blog/2011/10/01/anemic-vs-rich-domain-models/
  • Sources: https://github.com/link-intersystems/blog/tree/master/anemic-vs-rich-domain-model


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.


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


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.


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.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜