贫血领域模型: 优点/缺点

我想知道使用贫血领域模型的利弊(见下面的链接)。

福勒文章

16410 次浏览

It seems to me that Fowler's main objection is that ADMs are not OO, in the following sense. If one designs a system "from scratch" around passive data structures that are manipulated by other pieces of code, then this certainly smells more like procedural design than object-oriented design.

I suggest that there are at least two forces that can produce this kind of design:

  1. Designers/programmers who still think procedurally being required to work in an object-oriented environment (or assuming that they can...) to produce a new system, and

  2. Developers working to put a service-like "face" on a legacy system designed in a non-OO fashion (regardless of language).

If, for example, one were building a set of services to expose the functionality of an existing COBOL mainframe application, one might define services and interfaces in terms of a conceptual model that does not mirror the internal COBOL data structures. However, if the service maps the new model to the legacy data to use the existing-but-hidden implementation, then the new model might very well be "anemic" in the sense of Fowler's article -- e.g. a set of TransferObject-style definitions and relationships with no real behavior.

This kind of compromise may very well be common for the boundaries at which idealistically-pure OO systems must interact with an existing, non-OO environment.

An anemic domain model is an anti-pattern. Anti-patterns don't have pros.

The pros:

  • You can claim it's a domain model and brag to your developer friends and put it on your resume.
  • It's easy to generate automagically from database tables.
  • It maps to Data Transfer Objects surprisingly well.

The cons:

  • Your domain logic exists somewhere else, probably in a class full of class(static) methods. Or your GUI code. Or in multiple places, all with conflicting logic.
  • It's an anti-pattern, so other developers will ask if you understand the concepts of object oriented design.

"It's an anti-pattern, so other developers will ask if you understand the concepts of object oriented design."

"An anemic domain model is an anti-pattern. Anti-patterns don't have pros."

Whether the anemic domain model is an anti-pattern is a matter of opinion. Martin Fowler says it is, a number of developers who know OO inside out say it isn't. Stating opinion as fact is rarely helpful.

An, even if it was universally accepted to be an anti-pattern, the chances are it would still have some (though relatively little) upside.

"Developers working to put a service-like "face" on a legacy system designed in a non-OO fashion (regardless of language)."

If you think of many LOB applications, these legacy systems will often not use the same domain model as you do. The Anemic Domain Model solves this with the use of business logic in service classes. You could put all this interface code inside your model (in the traditional OO sense) - but you typically end up losing modularity.

It's the same pro as with most anti-patterns: it allows you to keep a lot of people busy for a long time. As managers tend to be paid more when they manage more people, there is a strong incentive not to improve.

With "Anemic Domain Model" being anti-pattern, why are there so many systems that implement this?

I think there are several reasons

1. Complexity of the system

In a simple system (which is almost all the examples and sample code you find on internet) if I want to implement:

Adding product to Order

I put this function on the Order

public void Order.AddOrderLine(Product product)
{
OrderLines.Add(new OrderLine(product));
}

Nice and super object oriented.

Now let's say that I need to make sure that I need to validate that the product exists in inventory and throw exception if it doesn't.

I can't really put it on Order any longer, since I don't want my order to be dependent on Inventory, so now it needs to go on the service

public void OrderService.AddOrderLine(Order order, Product product)
{
if (!InventoryService.Has(product)
throw new AddProductException


order.AddOrderLine(product);
}

I could also pass IInventoryService to Order.AddOrderLine, which is another option, but that still makes Order dependent on InventoryService.

There is still some functionality in Order.AddOrderLine, but usually it is limited to Order scope, while in my experience there is a lot more Business Logic out of Order scope.

When the system is more then just basic CRUD, you will end up with most of your logic in OrderService and very little in Order.

2. Developer's view of OOP

There are a lot of heated discussions on the internet about which logic should go on entities.

Something like

Order.Save

Should Order know how to save itself or not? Let's say we have repositories for that.

Now can Order add order lines? If I try to make sense of it using simple English, it doesn't really make sense either. User adds Product to Order, so should we do User.AddOrderLineToOrder()? That seems like overkill.

How about OrderService.AddOrderLine(). Now it kinda makes sense!

My understanding of OOP is that for encapsulation you put functions on classes where the function will need to access class's internal state. If I need to access Order.OrderLines collection, I put Order.AddOrderLine() on Order. This way class's internal state doesn't get exposed.

3. IoC Containers

Systems that use IoC containers are usually fully anemic.

It is because you can test your services/repositories which have interfaces, but can't test domain objects (easily), unless you put interfaces on all of them.

Since "IoC" is currently lauded as solution for all your programming problems, a lot of people blindly follow it and this way end up with Anemic Domain Models.

4. OOP is hard, procedural is easy

I have a bit of a "Curse of Knowledge" on this one, but I have discovered that for newer developers having DTOs and Services is a lot easier than Rich Domain.

Possibly it is because with Rich Domain it is more difficult to know on which classes to put the logic. When to create new classes? Which patterns to use? etc..

With stateless services you just slap it in the service with closest name.

The anemic domain model (ADM) may be a good choice if your team is unable or unwilling to build a rich domain model (RDM) and maintain it over time. Winning with an RDM requires careful attention to the dominant abstractions used in the system. Figure that, in any dev group, no more than one half and perhaps only one tenth of its members are competent with abstractions. Unless this cadre (perhaps only a single developer) is able to maintain influence over the whole group's activities, the RDM will succumb to entropy.

And the entropic RDM hurts, in particular ways. Its developers will learn harsh lessons. At first they will be able to meet the expectations of their stakeholders, because they will have no history to live up to. But as their system becomes more complicated (not complex) it will become brittle; the developers will try to reuse code but tend to induce new bugs or back-track in development (and thus overrun their estimates).

In contrast, the ADM developers will set lower expectations for themselves, because they won't expect to reuse as much code for new features. Over time they will have a system with many inconsistencies, but it probably won't break unexpecredly. Their time to market will be longer than with a successful RDM, but their stakeholders are unlikely to perceive this possibility.

In line with Eric P's answer as well as what some others above wrote, it seems that the main disadvantage of an ADM is the loss of OOD, specifically of keeping the logic and data of a domain concept together so that the implementation details are hidden while the API can be rich.

Eric goes on to point out that there's often information outside a domain class that is necessary for the logic of acting on that class, such as checking an inventory before adding an item to an order. I question, though, whether the answer is a Service layer that holds this overarching logic, or whether it's better handled as part of the object design. Somebody has to know about the Inventory object, the Product object and the Order object. Perhaps it's simply an OrderSystem object, which has an Inventory member, a list of Orders and so forth. This won't look very different from a Service, but I think it's conceptually more coherent.

Or look at it this way: You can have a User with an internal credit balance, and every time that User.addItemToOrder(item) is called it gets the item's price and checks the credit before adding it, etc. That seems a reasonable OO design. I'm not sure precisely what's lost by replacing that with Service.addItemToUserOrder(user, item), but I'm not sure what's gained, either. I guess a loss would be the extra layer of code, plus the clunkier style of writing and the enforced ignorance of the underlying Domain Model.

Following this, there has been a thought in my head for a very long time now. It is my belief that the term "OOP" has taken on a meaning not truly intended for it. The anagram mean "object oriented programming", as we all well know. The focus of course is on the word "oriented". It isn't "OMP", meaning "object mandated programming". Both ADM and RDM are examples of OOP. They make use of object, properties, methods interfaces and so forth. However, there is a difference between ADM and RDM in how we choose to encapulate things. They are two different things. To say that ADM is bad OOP is not an accurate statement. Maybe we need different terms for vaious levels of encapsulation instead. In addition, I never liked the term anti-pattern. It is usually assigned to something by members of an opposing group. Both ADM and RDM are valid pattern, they simple have different goals in mind, and are intended to solve different business needs. Those of us who practice DDD should, at the very least, appreciate this, and not fall to the level of others by bashing those who choose to implement ADM. Just my thoughts.

To extend Michael's answer I'd have thought it's (fairly) clear where that code should go: into a dedicated Mediator that handles the interaction betwene the Order and the Inventory.

From my POV the key thing about the domain is that it MUST hold the simple testing behaviour the isInThisState() methods etc. In my experience these are also scattered throughout the service tears (sic :)) in most companies, and either copied ar endlessly rewritten. All of which breaks standard conhesion rules.

In my view the approach should be to aim for a DM that holds as much of the biz beahaviour as is practical, put the rest in clearly designated areas (ie not in the services)

My team personally prefers the ADM. we have a set of business objects that represent specific parts of our domain. We use services to save these objects to the db. Our business objects do have methods, however these methods only manipulate it's internal state.

The benefit for us in using the ADM over RDM can be seen in how we persist the objects to db. Developers working on our legacy code systems can use our business objects(from the new system)and continue using their current data access layer to persist these objects to the db. Using the RDM would force developers of our legacy system to inject Repository objects into our business model...which would not be consistent with their current data access layer.

When I first came across the Anemic Domain Model article I thought "holy s***, that's what I do. horror!" I persevered and followed the references to Eric Evan's book, held to be a good example, and downloaded the source. It turns out that "not using an Anemic Domain Model" does not mean "not using service classes, not using mediators, not using strategies" or even "putting logic on the class being manipulated".

The DDD examples have service classes, XyzUpdaters, singletons and IoC.

I remain confused by exactly what an Anemic Domain Model is. I expect "I'll know it when I see it". For now I'm content with a positive example of good design.

It should be noted that as systems grow in complexity and granularity of variation, the encapsulation and consolidation of interface points afforded by a well designed message-passing object model make it much safer to change and maintain critical code without widespread refactoring.

The service layers created by the ADM, while certainly easier to implement (since they require relatively little thought and have many decentralized interface points) will likely create trouble down the road when it is time to modify a live and growing system.

I might add also that not all cases call for a domain model at all (let alone the ADM one). Sometimes it is better to use a more procedural / functional style of the task is data driven and does not depend on application-wide logic / business rules.

If you are trying to decide on the pros and cons for a whole app, I think it is important to first design what each one might look like for your given application BEFORE you even start writing a single line of code. Once you've CRC'd or wire-framed your application in both styles, take a step back and decide which one makes more sense and fits the application better.

Also think ahead to which one will be easier to maintain...

It gives better predictability. Managers like that, especially if the project is paid time & materials. Every change means a lot of work, so difficult work can be hidden behind lots of repetitive work. In a well-designed DRY system, predictability is very bad, as you are constantly doing new things.

Having worked with a 'mature' system with an ADM and feel I can provide some, at least, anecdotal feedback to this question.

1) Lack of Encapsulation

In a live system with an ADM there is the possibility to write e.g. 'obj.x = 100; obj.save', even if this violates business logic. This leads to a number of bugs that would not be encountered if the invariants were modelled on the object. It is the severity and pervasiveness of these bugs that I feel is the most serious negative to an ADM.

I feel it important to point out here that this is where a functional solution, and the procedural solutions of an ADM differ significantly and any similarities that others may have drawn to the surface similarities between an ADM and a functional solution are incidental.

2) Code bloat

I estimate that the amount of code produced in an ADM is 5-10x that which an OOP/RDM solution would create. This is accounted for by perhaps 50% being code repetition, 30% being boiler plate code, and 20% being the solving or resolving of problems which arise from the lack of a RDM.

3) Poor understanding of the domain problems

An ADM and a poor understanding of the domain problems go somewhat hand in hand. Naive solutions arise, requirements are poorly considered due to the difficulty of supporting them with the existing DM, and the ADM becomes a significant barrier to business innovation given the longer development times and lack of flexibility.

4) Difficulty of maintenance

A certain level of rigour is required to ensure that a domain concept is changed in all the places it is expressed, given that the concept may not be just a copy and paste re-implementation. This often leads to the same bugs being investigated and fixed on multiple occasions.

5) Increased difficulty with on-boarding

I think one of the benefits of a RDM is the cohesion of concepts which allow a faster understanding of the domain. With an ADM concepts may be fragmented and lacking clarity hence harder for new developers to acquire.

I was also tempted to include the operational support costs for an ADM being higher than for an RDM but this would depend on a number of factors.

As others have pointed out, look at DDD (Greg Evans, Vince Vaughn, and Scott Millett) for the benefits of a RDM.

After I first read Eric Evans book about Domain-driven design I did not really understand that it is not just a bunch of tactical patterns for designing good domain model classes.

After learning more about the topic and using the strategical patterns as well I finally started to understand that at first it is all about getting a deep understanding of the business problems you are trying to solve.

And only after that you can decide what parts of the system will fit for applying tactical patterns such as aggregates, entities, repositories, etc. along with so called rich domain models (as opposed to anemic ones). But in order to benefit from these patterns there has to be enough complexity concerning business logic for that part of the system.

So at when it comes to implementing the solution to the problem at hand it should first be determined if this specific problem is better approached with using a CRUD based approach or investing into a rich domain model along with the mentioned tactical patterns.

If CRUD makes more sense, e.g. if there is no complex business logic and most of the logic is concerned with transforming, transferring and persisting data implementing a domain model can be an unncessary overkill. This does not mean that there won't be a lot of work to do but simply the it's not the business rules which produce the most implementation effort. But in this case there is no such thing as an anemic domain model, simply because there is no domain model at all. What you will rather see are such things as DTOs (Data Transfer Objects) or DAOs (Data Access Objects) and service classes that will operate on the data. And the corresponding operations are to a high extent concerned with transforming data from one representation to another and moving data around with very little or almost no business logic at all.

If you determined that there is a lot of complex business logic which will also change over time than investing into a domain model is - from my experience - a good idea. The reason is that it is easier to represent the business perspective via code and make it easier to understand the corresponding operations that mirror the business domain and it's rules. This does not mean that there have to be domain model classes in every use case. For instance, if there is no state to be mutated and persisted there can also only be domain services which contain the domain logic are implemented more like pure functions.

But if there is also state to be mutated and persisted that also has purpose and meaning in the business domain the state and the behaviour that changes this state should be encapsulated. With that no one can get around the business rules that easy and by that lead to invalid states along with serious failures. So called anemic domain models are often sources of such problems. This is often the case if you see code where different components operate on the same "anemic" domain model class checking some part of it's state and changing some part of it's state without caring about or knowing the overall invariants of that business entity. It is not necessary to call this an anti-pattern but it is important to understand that you lose lots of benefits of rich domain models in a DDD based approach along with the mentioned problems. When using a domain model where behaviour and it's data are placed in the same class there can also be lot's of different "clients" calling operations of that class but they don't need to care that the business invariants of the business entity are adhered as the domain model class will always take care of that and can also inform the "client" about invalid operations or even throw exceptions as a safety net.

So bottom line, I think it is important to not confuse data-structure like classes (such as DTOs or DAOs) with anemic domain model classes. In a carefully and intentionally chosen CRUD based approach there is no advantage of trying to use a domain model because there is too less complex business logic.

By anemic domain model I would refer to code from which I can see that there is a lot of complex business logic and business rules that are spread across different components which should rather be close to the data these logic is changing.

There is also another lesson I learned along the way: if you try to use the same business language (also referred to as the Ubiquituous Language) in your code that the steakholders are using in their daily worklife you already win so many advantages concerning understanding of the business domain and improving the readability of your code that will help you so much no matter if you use a CRUD based or domain model based approach.