[Translation] Anemic domain model - not an anti-pattern, but SOLID architecture
From a translator: At the project where I work, an active rewriting of the logic that was previously implemented as a rich domain model (using Active Record and Unit of Work) is underway. The new approach includes classes of entities without behavior and stateless services interacting via interfaces - in fact, it is an anemic model, with the prospect of a transition to a microservice architecture in the future. Watching in real time how a “pasta monster” of about one and a half million LOC gradually takes shape, how testing, scaling and customization of the system to the needs of various customers is simplified, I was very surprised to learn that such an approach is often regarded as an architectural anti-pattern . Trying to figure out the reasons for this,
Original: The Anaemic Domain Model is no Anti-Pattern, it's a SOLID design
Design patterns, anti-patterns and anemic domain model
Speaking of object-oriented software development, design patterns mean repetitive and effective ways to solve common problems. Thanks to the formalization and description of such patterns, developers get a set of “battle-proven” architectural solutions for certain classes of problems, as well as a general dictionary for describing them that is understandable to other developers. Erich Gamma was the first to introduce this term in his book, "Object-Oriented Design Techniques. Design patterns ”[5], where he described several commonly used patterns. As the new concept gained popularity, the dictionary of design patterns expanded ([6], [17]).
Following the growing popularity of the concept of design patterns, the idea of “anti-patterns” was introduced into use ([7], [8]). As the name implies, an anti-pattern is the opposite of a pattern. It also describes a repeated way to solve a frequently occurring problem, however, as a rule, this solution is inoperative or ineffective, which negatively affects the system’s “health” (in terms of ease of support, extensibility, reliability, etc.). Anti-patterns serve the same purposes as patterns: when describing an anti-pattern, they show typical implementation options, reveal the context in which it is applied, and explain what problems this leads to in the developed software.
But the concept of anti-patterns has its drawback: a decrease in the criticality of perception regarding the applicability of a particular pattern. Architectural solutions that are not applicable in one situation may turn out to be a reasonable choice in another, however, if the solution is recognized as an anti-pattern, it can be rejected without discussion, even if in fact it was quite suitable for the problem being solved.
I am convinced that one of such undeservedly rejected anti-patterns is the Anemic domain model (AMA, Anaemic Domain Model), described by Martin Fowler [1] and Eric Evans [2]. Both authors describe this template as an inability to model a subject area in an object-oriented style, which is why business logic is described in a procedural style. This approach is contrasted with the Rich Domain Model (BMPO) [1], [20] - in it the classes representing the entities of the subject domain contain both data and all business logic. Yes, an anemic model may not be the right choice for some systems, but it’s not at all a fact that the same is true for any systems. In this article, I examine the arguments put forward against the anemic model and justify why, in a number of scenarios, AMPO seems to be a reasonable choice in terms of compliance with SOLID principles formulated by Robert Martin ([3], [4]), the principles that make recommendations for achieving a balance between simplicity, scalability and reliability in software development. By solving a hypothetical problem and comparing anemic and rich models, I intend to show that AMPO is better suited to SOLID principles. Thus, I want to challenge the categorical opinion of this approach, imposed by authorities, and show that the use of AMPO is, in fact, a suitable architectural solution. By solving a hypothetical problem and comparing anemic and rich models, I intend to show that AMPO is better suited to SOLID principles. Thus, I want to challenge the categorical opinion of this approach, imposed by authorities, and show that the use of AMPO is, in fact, a suitable architectural solution. By solving a hypothetical problem and comparing anemic and rich models, I intend to show that AMPO is better suited to SOLID principles. Thus, I want to challenge the categorical opinion of this approach, imposed by authorities, and show that the use of AMPO is, in fact, a suitable architectural solution.
Why is an anemic domain model considered an anti-pattern?
Fowler [1] and Evans [2] described AMPO as a set of classes without behavior, containing data necessary for modeling the subject area. In these classes there is practically no (or not at all) logic for validating data for compliance with business rules. Instead, business logic is enclosed in a service layer, which consists of types and functions that process model elements in accordance with business rules. The main argument against this approach is that the data and their processing methods are separated, which violates one of the fundamental principles of the object-oriented approach, because does not allow the model to provide its own invariants. In contrast, although the BMPO consists of the same set of types containing data about the subject area, but the whole business logic is also enclosed in these entities, being implemented as class methods. Thus, BMPO is in good agreement with the principles of encapsulation and information hiding. As noted by Michael Scott in [9]: “Thanks to encapsulation, developers can combine data and processing operations in one place, as well as hide unnecessary details from users of the generalized model.”
In BMPO, the service layer is extremely thin, and sometimes completely absent [20], and all the rules related to the subject area are implemented through the model. Thus, it is argued that the entities of the subject area are able to fully independently provide their own invariants, which makes such a model complete in terms of an object-oriented approach.
It should not be forgotten, however, that the ability of a model to fulfill certain constraints imposed on data is only one of the many properties that a system must possess. Although AMPO sacrifices the possibility of validation at the level of individual business entities, but in return it gives incredible flexibility and simplicity to support the system as a whole, due to the fact that the implementation of logic is carried out in highly specialized classes, and access to them is via interfaces. These advantages are especially important in languages with static typing, such as Java or C # (in which the behavior of the class cannot be changed at runtime), because improve the testability of the system by introducing explicit “seams” ([10], [11]) in order to eliminate excessive connectivity.
Simple example
Let's imagine the server part of an online store where a client can both buy goods and put up for sale goods for other customers from around the globe. The purchase of goods leads to a decrease in funds in the buyer's account. We’ll think about how you can implement the process of placing a customer order for the purchase of goods. According to the requirements, the client can place an order if he has a) sufficient funds in the account, and b) the goods are available in the client’s region. When using BMPO, the Customer class will describe the “Customer” entity; it will include all customer properties and methods such as PurchaseItem (Item item ). Similarly, the Item and Order classesrepresent domain models that describe the entities Product and Order, respectively. The implementation of the Customer class (in pseudo-C #) might be something like this:
/*** КОД С ИСПОЛЬЗОВАНИЕ БМПО ***/classCustomer : DomainEntity// Базовый класс, предоставляющий CRUD-операции
{
// Опускаем объявление закрытых членов классаpublicboolIsItemPurchasable(Item item)
{
bool shippable = item.ShipsToRegion(this.Region);
returnthis.Funds >= item.Cost && shippable;
}
publicvoidPurchaseItem(Item item)
{
if (IsItemPurchasable(item))
{
Order order = new Order(this, item);
order.Update();
this.Funds -= item.Cost;
this.Update();
}
}
}
/*** КОНЕЦ КОДА С ИСПОЛЬЗОВАНИЕ БМПО ***/
Entities of the subject area are implemented using the Active Record template [17], which uses Create / Read / Update / Delete methods (implemented at the framework or base class level) that allow you to modify records in the data storage layer (for example, in the database). It is assumed that the PurchaseItem methodIt is called as part of a transaction performed on a data warehouse and managed externally (for example, it can be opened in the HTTP request handler, which extracts information about the client and the product directly from the parameters passed in the request). It turns out that in our BMPO the role of the “Client” entity is 1) in representing the data model, 2) implementing business rules, 3) creating the “Order” entity for making a purchase, and 4) interacting with the data storage layer using methods specific to Active Record. Truly, the king of Croesus would envy the “wealth” of such a model, while we were considering a rather simple use case.
The following example illustrates how the same logic could be expressed using AMPO under the same conditions:
/*** КОД С ИСПОЛЬЗОВАНИЕМ АМПО ***/classCustomer { /* Some public properties */ }
classItem { /* Some public properties */ }
classIsItemPurchasableService : IIsItemPurchasableService
{
IItemShippingRegionService shipsToRegionService;
publicboolIsItemPurchasable(Customer customer, Item item)
{
bool shippable = shipsToRegionService.ShipsToRegion(item);
return customer.Funds >= item.Cost && shippable;
}
}
classPurchaseService : IPurchaseService
{
ICustomerRepository customers;
IOrderFactory orderFactory;
IOrderRepository orders;
IIsItemPurchasableService isItemPurchasableService;
// Конкретные экземпляры инициализируются в конструктореpublicvoidPurchaseItem(Customer customer, Item item)
{
if (isItemPurchasableService.IsItemPurchasable(customer, item))
{
Order order = orderFactory.CreateOrder(customer, item);
orders.Insert(order);
customer.Balance -= item.Cost;
customers.Update(customer);
}
}
}
/*** КОНЕЦ КОДА С ИСПОЛЬЗОВАНИЕМ АМПО ***/
Comparison of implementation examples in terms of compliance with SOLID principles
At first glance, AMPO is clearly losing to BMPO. More classes are used in its implementation, and the logic is spread over two domain services ( IPurchaseService and IItemPurchasableService ) and a number of application services ( IOrderFactory , ICustomerRepository and IOrderRepository ), instead of being located within the domain model. Domain classes now do not contain any behavior, but only store data and allow their state to change outside the framework of imposed restrictions (and - oh, horror! - lose the ability to provide their own invariants). Given all these obvious shortcomings, how can such a model be considered as a competitor to a much more object-oriented BMPO?
The reasons why AMPO is an excellent choice for this scenario stem from the consideration of SOLID principles and their imposition on both architectures under consideration [12]. “S” means the “Single Responsibility Pronciple,” [13], which states that a class should only do one thing — but do it well. In particular, a class should implement only one abstraction. “O” - “The principle of openness / closure” (Open / Closed Principle, [14]), the postulate that the class should be “open to expansion, but closed to change”. This means that when developing a class, you should strive to ensure that the implementation does not have to be changed in the future, thereby minimizing the consequences of the changes you make.
It would seem that the Customer class in BMPO implements the only “Customer” abstraction, but in fact this class is responsible for many things. This class models both data and logic within the same abstraction, despite the fact that business logic tends to change much more often than the data structure. The same class creates and initializes the entities "Order" at the time of purchase, and even contains logic that determines whether the customer can make a purchase. And by providing the basic CRUD operations defined in the base class, the essence of the Client subject area is also associated with the data warehouse model that is supported by the base class. As soon as we listed all these responsibilities, it became apparent that the essenceCustomer at BMPO is an example of a weak division of responsibility.
On the contrary, the anemic model divides the areas of responsibility in such a way that each component represents a single abstraction. Data from the subject area is presented in the form of “flat” data structures [18], while business rules and purely infrastructure tasks (saving, creating new instances of objects, etc.) are enclosed in separate services (and are accessible via abstract interfaces). As a result, the coherence of classes is reduced.
Comparison of the flexibility of solutions based on rich and anemic domain models
Consider examples of scenarios in which we would have to modify the Customer class in BMPO.
- You must add a new field (or change the data type of an existing one).
- An additional parameter must be passed to the constructor of the Order class .
- The business logic related to the purchase of goods has become more complicated.
- There was a need to store data in an alternative storage, which is not supported by our hypothetical base class DomainEntity.
Now consider the scenarios in which we need to change the types described in AMPO. Classes of business entities whose purpose is to model a domain are subject to change if and only if the requirements for data composition change. In the case of complicating the rules by which the possibility of acquiring a particular product is determined (for example, the minimum acceptable "customer confidence" rating for which this product can be sold is indicated for the product), only the implementation of IIsItemPurchasableService is subject to change , while when using BMPO I would have to modify the Customer class accordingly . If the requirements for the data warehouse change - in AMPO the problem is solved by transferring to the PurchaseServicefrom the higher class of application services, a new implementation of the existing repository interface [17], [19], without requiring modification of the existing code; in BMPO it’s so easy not to get rid of, the modification of the base class will affect all classes of business entities inherited from it. In the case when an additional parameter needs to be passed to create an instance of the Order class , the IOrderFactory implementation may be able to provide this change without affecting the PurchaseService . In the anemic model, each class has the sole responsibility, and changes to the class will only have to be made if the corresponding requirement in the subject area (or related infrastructure) changes.
Now let’s imagine that in order to implement a new business requirement, we must ensure the possibility of a refund if the customer is not satisfied with the purchase. In a rich model, this could be implemented by adding the RefundItem method to the “Client” entity, arguing that all the logic related to the client is enclosed in the Customer entity . However, the refund procedure is very different from the purchase procedure for which responsibility was previously assigned to the Customer class, and as a result we get an even greater confusion of responsibilities within the same type. It turns out that weakly connected elements of business logic can accumulate in the classes of a rich model, increasing the complexity of their structure. In the anemic model, the money back mechanism can be implemented by creating a new class RefundService , which will only implement logic directly related to returns. This class may depend on several other abstractions (i.e., the interfaces of other domain and infrastructure services) needed to fulfill its responsibilities. Accessing RefundService Class Methodsmay come from higher levels (in response to a request for a refund), and it turns out that the implementation of the new scenario was completed without any influence on the previously developed functionality.
In the considered example, the problem of assigning to one class of unrelated responsibilities that we encountered in BMPO is effectively solved in the anemic model using the letters I and D from the abbreviation SOLID. This, I recall, is the “Interface Segregation Principle,” [15] and the “Dependency Inversion Principle,” [16]. They argue that interfaces should be sets of tightly coupled methods, and that interfaces should be used to connect parts of the system together (in the case of AMPO, the connection of domain layer services to each other). Following the principle of interface separation typically results in small, highly specialized interfaces - such as IItemShippingRegionService and IIsItemPurchasableServicefrom our example, or the abstract repository interface. The principle of dependency inversion forces us to rely on these interfaces so that one service does not depend on implementation details of another.
Anemic domain model better supports automated testing
A more flexible and flexible application structure, as well as following the above principles, allow the anemic model to show its advantages over BMPO in simplifying automated testing. Strongly coupled, but weakly interconnected components communicate via interfaces and are assembled through the implementation of dependencies, which allows you to easily replace dependencies with "dummies", mock-objects. Hence, in AMPO it is not difficult to implement such scripts for automated testing, which would be much more difficult to implement in the framework of BMPO, thereby improving the ease of support for automated tests. While reducing the “cost” of automated tests, developers are more willing to create and maintain them up to date. As an illustration, let's try to develop a unit test for a methodIsItemPurchasable .
According to the requirements, the goods are considered available for purchase if the client has enough funds in the account and is located in the region where the goods can be delivered. Suppose we write a test verifying that if the client has enough funds in the account, but he is not in the region where this product is delivered, then this product is not available for purchase. In BMPO, such a test would probably include creating instances of the Customer ( Customer ) and Product ( Item ), setting up the Customer so that the funds on his account exceed the cost of the Goods, and that his region is not included in the list of regions where this product is delivered. Then we would have to make sure that customer.IsItemPurchasable (item) returns false. However, the IsItemPurchasable method depends on the implementation details of the ShipsToRegion method of the Item class . Changing the business logic for a product will result in a change in the results of this test. This effect is undesirable, since this test should only check the logic enclosed in the Customer class , and the logic of the ShipsToRegion method, enclosed in the essence of “Product”, should be covered by a separate test. Since the business logic is enclosed in entities that describe the subject area and provide an open interface for accessing the logic enclosed in them, the classes are strongly connected, which leads to an avalanche effect when changes are made, which makes automated tests fragile.
On the other hand, in AMPO, the logic of the IsItemPurchasable method is moved to a separate specialized service, which depends on abstract interfaces ( IItemShippingRegionService.ShipsToRegion method ). For the test in question, we can simply create a stub for IItemShippingRegionService , in which the ShipsToRegion method will be implemented , always returning false. By dividing the business logic into isolated modules, we protected each part from changes to implementation details in other parts. In practice, this means that a small change in the logic will most likely lead to a “crash” of only those tests that directly verify the behavior of the code in which the changes were made, which can be used to verify the correctness of our understanding of the code being modified.
BMPO refactoring to comply with SOLID principles leads to model “anemia”
Proponents of architecture using BMPOs may argue that the hypothetical example described does not match the “true” rich model. They will say that in a properly implemented rich model, one cannot mix domain entities with tasks for writing them to the storage - instead, it is preferable to use data transfer objects (DTO, Data Transfer Object, [17], [18]), through which they exchange with data storage layer. They will smash the idea of directly invoking the constructor of the Order class directly from the logic of the Customer class- of course, in no sane implementation the entities of the subject area will not call the constructor directly, common sense forces us to use the factory [5]! But for me, it looks like an attempt to apply the power of SOLID principles to infrastructure services, while completely ignoring them in the application to the domain model. If our hypothetical BMPO refactor to meet SOLID principles, will be allocated to smaller entities: the essence of the client can be isolated essentially "Customer Purchase» ( CustomerPurchase ) and "Return customer den.sredstv» ( CustomerRefund) But it may turn out that new models will continue to depend on elementary business rules that are changed independently of each other, and other entities, in turn, will depend on them. In order to avoid duplication of logic and strong cohesion of classes, these rules will have to be further refactored, highlighting them in separate modules, which are accessed through interfaces. As a result, a rich model, refactored to full compliance with SOLID principles, strives for the state of an anemic model!
Conclusion
Having examined the implementation of a simple example, we came to the conclusion that the anemic domain model is closer to SOLID principles than the rich model. We saw the benefits of being compliant with SOLID principles: poor connectivity and strong cohesion to increase the flexibility of the application architecture. Evidence of increased flexibility was the improved testability of the application due to the ease of implementation of "stubs" for dependencies. Considering ways to achieve the same qualities in the framework of BMPO, we found that refactoring a rich model naturally leads to its “anemicity”.
Thus, if compliance with SOLID principles is a sign of a well-structured object-oriented application, and the anemic model is closer to these principles than a rich model, then the anemic model should not be considered an anti-pattern; it should be considered as a viable version of architecture when modeling a domain.
References
[1] Fowler, Martin. Anaemic Domain Model. http://www.martinfowler.com/bliki/AnemicDomainModel.html, 2003.
[2] Evans, Eric. Domain-driven design: tackling complexity in the heart of software. Addison-Wesley Professional, 2004.
[3] Martin, Robert C. The Principles of Object-Oriented Design. http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod, 2005.
[4] Martin, Robert C. Design principles and design patterns. Object Mentor, 2000: 1-34.
[5] Erich, Gamma, et al. Design patterns: elements of reusable object-oriented software. Addison Wesley Publishing Company, 1994.
[6] Wolfgang, Pree. Design patterns for object-oriented software development. Addison-Wesley, 1994.
[7] Rising, Linda. The patterns handbook: techniques, strategies, and applications. Vol. 13. Cambridge University Press, 1998.
[8] Budgen, David. Software design. Pearson Education, 2003.
[9] Scott, Michael L. Programming language pragmatics. Morgan Kaufmann, 2000.
[10] Hevery, Miško. Writing Testable Code. http://googletesting.blogspot.co.uk/2008/08/by-miko-hevery-so-you-decided-to.html, Google Testing Blog, 2008.
[11] Osherove, Roy. The Art of Unit Testing: With Examples in. Net. Manning Publications Co., 2009.
[12] Martin, Robert C. Agile software development: principles, patterns, and practices. Prentice Hall PTR, 2003.
[13] Martin, Robert C. SRP: The Single Responsibility Principle. http://www.objectmentor.com/resources/articles/srp.pdf, Object Mentor, 1996.
[14] Martin, Robert C. The Open-Closed Principle. http://www.objectmentor.com/resources/articles/ocp.pdf, Object Mentor, 1996.
[15] Martin, Robert C. The Interface Segregation Principle. http://www.objectmentor.com/resources/articles/isp.pdf, Object Mentor, 1996.
[16] Martin, Robert C. The Dependency Inversion Principle, http://www.objectmentor.com/resources/articles/dip.pdf, Object Mentor, 1996.
[17] Fowler, Martin. Patterns of enterprise application architecture. Addison-Wesley Longman Publishing Co., Inc., 2002.
[18] Fowler, Martin. Data Transfer Object. http://martinfowler.com/eaaCatalog/dataTransferObject.html, Martin Fowler site, 2002.
[19] Fowler, Martin. Repository. http://martinfowler.com/eaaCatalog/repository.html, Martin Fowler site, 2002.
[20] Fowler, Martin. Domain Model. http://martinfowler.com/eaaCatalog/domainModel.html, Martin Fowler site, 2002.