
When we start a greenfield project, everything goes smoothly, we add new features and write tests for them. With each new functionality, the project’s accidental complexity grows. When after a year or two it goes into production, the situation is no longer as colorful as at the beginning, each change in logic causes changes in several places, and there are still tests to be improved. The project’s maintainability drops drastically.Then we start to wonder. We stuck to S.O.L.I.D, we watched the quality in code review. What happened? This could be a symptom of “Anemic Domain Model” disease.
What is this “Anemic Domain Model”?
An anemic domian model is an entity that only has { get; set; }, there are no methods. Usually there is only a representation of a database table. The next attribute of “anemic entity” is that its properties are public. Otherwise, the “service”, “manager” or “command handler” would not be able to do any business logic on it.
Let’s look at an example.
Domain Layer:
namespace AnemicModel.Domain.Orders
{
public sealed class Order
{
public long Id { get; set; }
public Guid CustomerId { get; set; }
public OrderStatus Status { get; set; }
public decimal TotalPrice { get; set; }
}
public class OrderItem
{
public Product Product { get; set; }
public Order Order { get; set; }
}
}
Application Layer:
namespace RichModel.Application.Orders.Commands.CreateOrder
{
internal sealed class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand>
{
private readonly IOrdersRepository _ordersRepository;
private readonly IProductsRepository _productsRepository;
public CreateOrderCommandHandler(IOrdersRepository ordersRepository,
IProductsRepository productsRepository)
{
_ordersRepository = ordersRepository;
_productsRepository = productsRepository;
}
public async Task<Unit> Handle(CreateOrderCommand createOrderRequest, CancellationToken cancellationToken)
{
var productsToOrder = await _productsRepository.GetAsync(createOrderRequest.ProductsIds, cancellationToken);
var order = Order.Create(productsToOrder);
await _ordersRepository.CreateAsync(order, cancellationToken);
return Unit.Value;
}
}
}
A lamp should light us up here? When we started learning OOP, we found out that
Hermetization is a pillar of this OOP paradigm
but forgot this now and set most of properties as public.
If we create CRUD type functionalities, e.g. the functionality of adding countries to the database. There will be no logic associated with them, then the ‘anemic entity’ is a good way, there is nothing to do overengennering.
Cure for anemic entities – Rich Model Design Approach
Let’s go back to the roots, basics of object-oriented programming. According to wikipedia:
Object-oriented programming (OOP) is a programming paradigm based on the concept of “objects”, which can contain data and code: data in the form of fields (often known as attributes or properties), and code, in the form of procedures (often known as methods).
Rich models has data and behaviours like. Our “anemic enitities” has only data. We should move our business logic that is in application layer to domain entity. Then we will have rich model, classes equal to the OOP class definition.
Let’s go to example.
Domain Layer
namespace RichModel.Domain.Orders
{
public sealed class Order
{
private const decimal DiscountPercent = 0.25m;
private const decimal PriceBoundaryToCalculateDiscount = 1000;
public Guid Id { get; }
private OrderStatus Status { get; }
public decimal TotalPrice => CalculateTotalPrice();
private ICollection<OrderItem> Items { get; set; }
private Order()
{
}
private Order(List<Product> products)
{
Id = Guid.NewGuid();
Status = OrderStatus.Payed;
var readonlyProducts = products.AsReadOnly();
SetItems(readonlyProducts);
}
public static Order Create(List<Product> products)
{
return new Order(products);
}
private void SetItems(IReadOnlyCollection<Product> products)
{
if (AreProductsEmpty())
throw new OrderCannotBeEmptyException();
Items = products.Select(product => OrderItem.Create(product, this)).ToList();
bool AreProductsEmpty()
{
return products is null || !products.Any();
}
}
private decimal CalculateTotalPrice()
{
var amount = Items.Select(orderItem => orderItem.Product.Price).Sum();
if (amount <= PriceBoundaryToCalculateDiscount) return amount;
return amount * (1 - DiscountPercent);
}
}
}
Application Layer
namespace RichModel.Application.Orders.Commands.CreateOrder
{
internal sealed class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand>
{
private readonly IOrdersRepository _ordersRepository;
private readonly IProductsRepository _productsRepository;
public CreateOrderCommandHandler(IOrdersRepository ordersRepository,
IProductsRepository productsRepository)
{
_ordersRepository = ordersRepository;
_productsRepository = productsRepository;
}
public async Task<Unit> Handle(CreateOrderCommand createOrderRequest, CancellationToken cancellationToken)
{
var productsToOrder = await _productsRepository.GetAsync(createOrderRequest.ProductsIds, cancellationToken);
var order = Order.Create(productsToOrder);
await _ordersRepository.CreateAsync(order, cancellationToken);
return Unit.Value;
}
}
}
Now the roles have changed, our domain is rich in business logic and properly encapsulated, and the application layer is simple.
The real strength of this approach can be seen in tests.
The logic of calculating discounts tested and We don’t not had to write a single mock. As you can see, the tests are legible and easy to write.
namespace RichModel.Domain.UnitTests.Orders
{
public class OrderTotalPriceTests
{
private static object[] _orderDiscountNotCountedCases =
{
new object[] { 999.0m, 1.0m, 1000m },
new object[] { 555.0m, 445.0m, 1000m },
new object[] { 333.0m, 333.3m, 666.3m }
};
[TestCaseSourceAttribute(nameof(_orderDiscountNotCountedCases))]
public void Given_GetTotalPrice_When_ProductsPriceAmountLessenThanPriceBoundaryToCalculateDiscount_Then_DiscountIsNotCounted
(decimal firstProductPrice, decimal secondProductPrice, decimal expectedOrderTotalPrice)
{
var products = new List<Product>
{
Product.Create(firstProductPrice),
Product.Create(secondProductPrice),
};
var order = Order.Create(products);
var orderTotalPrice = order.TotalPrice;
orderTotalPrice.ShouldBe(expectedOrderTotalPrice);
}
private static object[] _orderDiscountCountedCases =
{
new object[] { 2000.0m, 1000.0m, 2250m },
new object[] { 2500.0m, 1500.0m, 3000m },
new object[] { 999.0m, 2.0m, 750.75m },
};
[TestCaseSourceAttribute(nameof(_orderDiscountCountedCases))]
public void Given_GetTotalPrice_When_ProductsPriceAmountGreaterThanPriceBoundaryToCalculateDiscount_Then_DiscountIsCounted
(decimal firstProductPrice, decimal secondProductPrice, decimal expectedOrderTotalPrice)
{
var products = new List<Product>
{
Product.Create(firstProductPrice),
Product.Create(secondProductPrice),
};
var order = Order.Create(products);
var orderTotalPrice = order.TotalPrice;
orderTotalPrice.ShouldBe(expectedOrderTotalPrice);
}
}
}
Summary
The biggest problem in “Anemic Domain Model” approch is that business logic is in wrong place, the application layer (Services, Managers, Command Handlers) and not in the domain layer, which causes encapsulation to suffer. When you exposing to much other developers, can use this wrong context or layer. In result we have a spaghetti code, maintability drops with each feature.
We should move your logic to entities and encapsulate properly, then our code will be realy open/close and maintabily our system will drops much slower.
Questions
- How save rich entity in database? The answer is “Shodow Properties”.
- How to display data on UI from an entity if the properties are private? The answer is CQRS.
More detailed answer will be in future posts.