Encapsulating the business logic into entities is especially difficult when the logic involves something more. Something more could be a different module, external system, database, …
We have a Cart in the online store system, the Cart is responsible for handling items (add, remove) but not prices. Once we want to show the total price of the Cart, we have to use fresh prices from the database because product prices are changed often.
Logic in Service
Let’s start with a service implementation that can be usually found in large applications (usually and sadly). The benefit of this implementation is that it works
We can use Doctrine, our favorite ORM or a different database layer instead of PDO, but the code will be similar.
As we can see, we collect data from Cart entity and then do the business logic. This approach is difficult to test, breaks encapsulation, leads to an anemic model with a logic outside of entities. Not so good outcome.
Think About Use-Case Only
The theme of this article is logic in entities, but let’s think about the use-case Calculate Cart Total Price without programming first. To be able to finish the use-case we need
- a Price of each Product
- Amount of each of Products
That’s all we need. It doesn’t matter if the use-case is implemented in service or entity, we need all this information in both implementation.
Moving Logic to Cart Entity
Our goal is to implement calculating the total price in the Cart entity itself.
We already know the amount of each of the products in the Cart, so there is no problem.
But we also need to know the price of each Product in the Cart. We don’t need to know prices in the whole Cart, we need to know prices only during the calculation of the total price. So if we pass Product Prices just to the calculating method, we solve the problem.
And we can achieve Product Prices by an interface
Now we can pass this interface into the calculation method that sits in the Cart
As we can see, the method is logically as same as it was in
CartTotalPriceService. But there are a couple of important differences
- The logic sits together with data in the Cart entity, this leads to a rich model
- The Cart doesn’t expose data (Items), Items are therefore properly encapsulated
- This code is testable as we can mock the interface in tests to produce predictable prices
- By the way, this was our goal — to have the logic in the entity
The interface has to be implemented anyway, but that’s not a problem at all
Do you remember
CartTotalPriceService? The code is the same.
And again we can use our favorite ORM, or database layer to implement the interface. We can do even more, the implementation can use different system module or even API to a different system. We are not limited by technology, all thanks to the interface.
Only One Implementation
We will usually have only one implementation for such an interface, that is strange, isn’t it wrong?
We are using interface to separate layers in this case. We separate the business logic layer and infrastructure (database) layer. And that is a valid interface usage.
Moving Logic … even further
Do you think the logic is moved well? I have an advanced proposal.
An Item is probably a value object an moving the logic into value objects is even greater than moving the logic to entities. That’s because value objects are immutable and the logic is, therefore, super easily testable and super separated.
So let’s move some logic into the Item
Even the Item has behavior now. The Item doesn’t expose the data and contains the logic that directly uses the item itself.
Maybe this paragraph isn’t only for maniacs, but programmers are usually shocked just by the interface, so I decided to calm down a bit.
Moving logic to entities is possible, use an interface that provides the necessary information and passes it to the method. Logic in entities is easy to understand, leads to better testability, better layer separation, and better encapsulation.