Anemic vs. Rich Domain Objects—Finding the Balance
Are you pro-Anemic Domain Model or pro-Rich Domain Object? This article looks at both and seeks to find a balance between the two approaches.
Join the DZone community and get the full member experience.
Join For FreeWhat prompted another article on this topic?
I just read an old article which, in an attempt to avoid Anemic Objects, the author was showing how to make it possible for a User Entity (Domain Object) to inject a DAO so that the User object can become a Rich Domain Object. I have read similar articles, some pro-Anemic Domain Model, and others pro-Rich Domain Object—the problem is that these are normally on the extreme of the two sides.
Rich Domain Object Extreme
The domain object does everything on its own in hosting JVM memory state, and also the external state of the object, that being database state, local filesystem state, etc.
@Entity
public class Order implements Serializable {
private static final long serialVersionUID = 8286951137068706345L;
private List<OrderItem> orderItems;
@Inject
private OrderDAO orderDAO; //using composition doesn't make it acceptable
//Responsibility 1: domain behaviour using own in JVM memory state
public BigDecimal getTotalPrice(){
BigDecimal total = BigDecimal.ZERO;
if(orderItems != null){
total = orderItems.stream()
.map(OrderItem::getTotalPrice)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
return total;
}
//Responsibility 2: fetching external state
public Order find(long orderId){
return orderDAO.find(orderId);
}
}
Anemic Domain Model Extreme
Services/Managers are resposible for both orchastration of different layers and for providing domain behavior, answering all the domain questions using dumb domain objects that act as bags of data. Ironically, this double responsibility was reached while trying to achieve a single responsibility. I have been guilty of writing such code myself...sigh!
POJO != Dumb Object
public class OrderService {
private final OrderDAO orderDAO;
private final PaymentProcessorFactory paymentProcessorFactory;
@Inject
public OrderService(OrderDAO orderDAO, PaymentProcessorFactory paymentProcessorFactory){
this.orderDAO = orderDAO;
this.paymentProcessorFactory = paymentProcessorFactory;
}
//Responsibility 1: orchastrating different layers and services
//using DAO layer to ask for Order's external state, finding PaymentProcessor
public String makePayment(PaymentType paymentType, long orderId) {
PaymentProcessor paymentProcessor = paymentProcessorFactory.getProcessor(paymentType);
Order order = orderDAO.find(orderId);
BigDecimal totalPrice = getTotalPrice(order);
//lets hope over time no one ever mistakenly changes Order state
//like removing an Item from the order here and forgets to recalculate
String paymentRef = paymentProcessor.process(orderId,totalPrice);
order.setPaymentRef(paymentRef);
orderDAO.update(order);
return paymentRef;
}
//Responsibility 2: domain behaviour
private BigDecimal getTotalPrice(Order order){
BigDecimal total = BigDecimal.ZERO;
if(orderItems != null){
total = orderItems.stream()
.map(item -> {
return item.getPrice()
.multiply(new BigDecimal(item.getQuantity()));
})
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
return total;
}
}
Where do we find that balance?
I have 3 hard rules that have worked well for me so far:
Operations/methods added to a Domain Object should mutate ONLY in the current JVM memory state of that object, and NOT any external state (i.e. state in database).
Operations in the orchastrating objects (Manager/Service) should NOT break if the state of a Domain Object changes. This can be achieved if they always ask domain objects for answers to domain questions instead of trying to figure out the answers themselves.
Operations on the Domain Object should ONLY use the in current JVM memory state of the object.
Example of Rule 2 & 3:
An Order object may have an operation to calculate the total price of the collection of OrderItems that make up the Order object's state. The anemic approach to this same use case is not safe because some changes in the Order object's state (like removing one OrderIterm) will break the calculation, as seen in the Anemic Domain Model Extreme example above.
After Refectoring The Two Extremes
The OrderItem object can give us its total price taking quantity into consideration:
@Entity
public class OrderItem implements Serializable {
private static final long serialVersionUID = 3488387236558906526L;
private BigDecimal price = BigDecimal.ZERO;
private int quantity = 0;
public BigDecimal getTotalPrice(){
return price.multiply(new BigDecimal(quantity));
}
}
The Order object can give us its total order price using the collection of OrderItems that make up its state:
@Entity
public class Order implements Serializable {
private static final long serialVersionUID = 8286951137068706345L;
private List<OrderItem> orderItems;
public BigDecimal getTotalPrice(){
BigDecimal total = BigDecimal.ZERO;
if(orderItems != null){
total = orderItems.stream()
.map(OrderItem::getTotalPrice)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
return total;
}
}
The OrderService object asks domain objects and other layers questions, then uses the answers to those questions to:
Create domain objects, or
Provide those answers as input parameters to mutate domain objects, or
Provide those answers as input parameters to question other layers or services
public class OrderService {
private final OrderDAO orderDAO;
private final PaymentProcessorFactory paymentProcessorFactory;
@Inject
public OrderService(OrderDAO orderDAO, PaymentProcessorFactory paymentProcessorFactory){
this.orderDAO = orderDAO;
this.paymentProcessorFactory = paymentProcessorFactory;
}
public String makePayment(PaymentType paymentType, long orderId) {
//find external service to use
PaymentProcessor paymentProcessor = paymentProcessorFactory
.getProcessorByType(paymentType);
Order order = orderDAO.find(orderId); //bring external state into JVM
//ask external service to process, then give back reference
String paymentRef = paymentProcessor.process(orderId,order.getTotalPrice());
order.setPaymentRef(paymentRef); //update with Order payment external state
orderDAO.update(order); //ask DAO to update external state (database)
return paymentRef;
}
}
Reasons Behind the Rules
The most read articles discussing this topic came after the famous Martin Fowler AnemicDomainModel article, and what I have taken away from these articles is that we often misinterpret or imagine that an article we are reading is saying more than what the author intended because we missed some points, then we go ahead and form our own Isis or Boko Haram just in case someone else dares have a different view to what our favorite author "said," (do NOT miss the quotes). Martin's article shows that there is a right place (balance) for all operations or behaviour as it clearly states:
It's also worth emphasizing that putting behavior into the domain objects should not contradict the solid approach of using layering to separate domain logic from such things as persistence and presentation responsibilities. The logic that should be in a domain object is domain logic - validations, calculations, business rules - whatever you like to call it.
Using the mixing of persistance in the domain object as an example, I am of the opinion that any object's external state should be accessed or persisted behind some layer—call it DAO layer—because although the object's in-JVM-memory state may be similar to the external state at some point—be it database or local filesystem state—the in-JVM-memory is only a representation of the external state, not the same object.
Conclusion
This article's title is about finding a balance, but is there actually one to be found? I do not think so.
We need Services/Managers to orchastrate our domain objects, which involves asking DAO layers to fetch us the external state of our domain data so we can have their representation in JVM memory as domain objects; then our services/managers can ask these domain objects the right questions to provide input into other DAOs and services that are either internal or external. Services/Managers are orchastraters, not domain experts.
Domain Objects should only be responsible for mutation and providing all behavior related to their own in hosting JVM memory state.
These are different responsibilities, and this argument should never have existed in the first place. My opinion of Martin's article is that it serves as a reminder that the responsibilities of Domain Objects and Services/Managers are different, and each should play its own role to the full.
Opinions expressed by DZone contributors are their own.
Comments