Anemic Domain Model in Typical Spring Projects (Part 2)
Anemic Domain Model in Typical Spring Projects (Part 2)
Continue learning about the anemic domain model, its benefits and drawbacks, and how to apply it to enterprise Spring projects.
Join the DZone community and get the full member experience.Join For Free
(Continued from Part 1)
Rich Domain Model Extreme could be an alternative, but in my opinion, you shouldn’t inject infrastructure classes to domain classes. Such code will quickly become unsupported and hell for testing, because every programmer has their own style of coding and in practice, this often violates conventions and good practices. Therefore, experience is tremendously important in this case. That is why we have to find some balance in anemic and rich models. In order to get a deeper knowledge of this topic, I would recommend reading this article.
What is important for us is that the author recommends sticking to three rules:
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 a database).
Operations in the orchestrating 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 current JVM memory state of the object.
Now we can see the depth of the problem. Both approaches have serious flaws. It is worth a try to make a hybrid of two models. On the one hand, it will be an enhanced anemic model, but on the other hand, we should beware that it could inherit the drawbacks of both models. So in our services, we will place only methods that use infrastructure, and in the model, we place business logic that operates with fields of the class.
In that case, the domain classes will look like this:
It is easy to cover a class like this with tests. UserService will use the model's methods. Just insert a few mocks in the service and an integration test is ready!
One more thing about rules 1 and 3. In my opinion, there could be an exception. If we use lazy loading inside the model and it works with satisfactory performance, we can unstick. Moreover, there are ways to efficiently use this approach, for example the @Fetch annotation:
In addition to all of the above, it should be noted that even if all best practices are followed, refactoring sometimes CANNOT be done due to external constraints. First and foremost, a database limits us in many ways. We cannot just move fields from one object to another or change them because we have to make appropriate changes in the DB, which could be impossible. Sometimes other projects use the same DB directly, which is not a very good idea. Do you remember that we should use interfaces to work with classes? The same rule applies to other services.
As already mentioned, the use of an anemic domain model leads to the impossibility or reduction of the efficiency of many refactoring techniques and design patterns.
For example, Builder has great use in the model. You can easily move long methods of model objects initialization into the objects themselves. The service will no longer have methods like initExpenseEntity (expenseDTO) with 20 lines of setParameter (value)… We can add methods that initialize fields with default values, accept DTOs or other objects, etc. The main thing is that this code will not be in the service now.
Then the creation of a new object in ExpenseService will look like:
Wait a second! Isn't this code a violation of the Creator (GRASP) pattern? Yes, it is! Generally speaking, almost half of the GoF patterns in some way violate something. For example, Visitor hates OOP, but sometimes there are situations when you just can't avoid using it. As we have already seen, it is not possible to write an enterprise project adhering to every principle, practice, and pattern. So we should ask ourselves that if we violated something, what consequences and benefits would it cause? In Builder's case, it will not mess up code. This pattern is hard to ruin. It is easy to understand its purpose and use it, especially for new programmers on a project. Therefore, its use is more than justified.
Many refactoring techniques are also unavailable. For example, Replace Method with Method Object: When we replace a method with a class that has the name of the method and in the constructor, it expects an instance of the caller class. The new class has only one method, compute(), which does the same thing as the old method. To call a new method, we create the new object and pass the caller class itself to it: new MethodObject(this). In the anemic model, the problem is simply moved to another layer, because it makes no sense to pass the object of the service as a parameter because the information is stored in local variables.
In our examples, it's hard to come up with some complex logic to justify using this technique, but imagine that we need to calculate all the expenses of a particular user based on many class fields. In this case, our method will not have local variables, and all of them will become fields of the new class, which can significantly improve the readability of the method. Then it will be easy to split the method into many smaller ones that will not pass 4 parameters from method to method. It will look like this:
Polymorphism in Domain Model
How can we use all of it in a Spring environment? We shall consider all of the limitations of the selected architecture, external services, and Spring itself. We know that RDBMSs don't support polymorphism, but there is a way to use it in the domain model, which is mapped to DB tables.
Do you remember the example of Replace Conditional With Polymorphism? Imagine that instead of ExpenseEntity in an anemic model we want to have abstract ExpenseEntity, which contains all shared fields and methods and its descendants: BasicExpenseEntity, SaleExpenseEntity, WholesaleExpenseEntity? Every class wants to have his own fields.
We have two options to achieve this. Use a single table that contains all of the columns, but then we need to make sure all NOT NULL constraints are enforced by triggers. Or we may use one table for common fields and separate tables for every subclass that will be connected by a Foreign Key. The first approach does not support Not Null constraints, which have to be handled by triggers. The second uses JOINs, which could be an expensive operation for large numbers of records if you use 4+ statements.
Example of polymorphic domain model:
What the purpose of all of this? "Any fool can write code that a computer can understand. Good programmers write code that humans can understand." I know that you have heard it many times. But do you really understand what it's for?
For businesses, one of the most important things is how quickly a developer can make new functionality. For the developer, the main factor is the quality of the existing codebase (not taking into account the experience of the programmer). If our architecture and code are clean, the development process goes quickly, it saves the project owner's money, and increases our professional skill. But if we have spaghetti code, it could sometimes be impossible to add a new feature without refactoring the whole project, which can take weeks or even months of hard work.
I am sure that you are familiar with bugs that you need to fix quickly. We figured out what we need to do on the front-end, it calls some method on the server, we turn on the debug, and go through the long chains of methods in the service layer. Finally, there is a couple of 20+-line-long methods and in the end, you have found the problem. It takes a lot of time. You have to look at a lot of code that you are not interested in. It is much better to split the methods and keep the logic of the model in the model itself and access it in services that are responsible only for infrastructure logic. Then the number of methods will increase, which will have a positive effect on the readability of the code, as each method's name will be a kind of comment for the code it holds. The code will be encapsulated in separate classes. It would be easier to maintain the project, changes would be faster and cheaper, and developers would be more productive.
The Rich Domain Model sounds great in theory, as it allows you to write more quality code with fuller use of OOP — but it requires a professional team with programmers who know each other's programming style. On the other hand, the Anemic Domain Model is much easier to implement and maintain if you don't have a very experienced team, but it greatly cuts down on design capabilities and promotes a procedural programming style. Therefore, in the real world, you should try to find the balance and move logic into the domain model and leave the calls in services.
Such a "hybrid" approach gives us a significant advantage — we can divide the methods and put logic into the appropriate layers. This approach clearly splits the Model layer in the MVC architecture into Model and Service, makes test writing easier, and facilitates cleaner code by grouping methods more logically.
Opinions expressed by DZone contributors are their own.