这是一篇质疑DDD的好文,后面也贴了一个回复,原文链接:http://techblog.bozho.net/?p=180
Eric Evans has formulated what domain-driven design (DDD) is. Martin Fowler is a great supporter and advocate of DDD. These are remarkable names and it is almost certain they are supporting something worth. And I’m not here to argue with that. Maybe I’m trying to justify the way I’ve been writing software, or maybe I’m trying just to clear things and be constructive. Let’s see.
What is at the core of domain-driven design – the abstract notions of the domain, the domain model, the ubiquitous language. I’ll not go into details with that – for those interested there is wikipedia (with lots of references to read in the footer). This is all very good in theory, and the domain-driven way of building software should appeal to everyone – after all that software is being built for the benefit of that domain, not for the benefit of architects, developers or QAs.
But now comes the practical part – how to implement DDD? I’ll answer that question in its contemporary context – that is, using frameworks like spring and hibernate. And I’ll justify their usage. Spring is a non-invasive dependency-injection framework. DI is also strongly supported by Fowler, and is considered a good way to implement DDD. Hibernate is one way to use objects when working with a relational database. Another way is to use JDBC and construct the objects manually, but that is tedious. So hibernate doesn’t influence the architecture part – it is an utility (very powerful one, of course).
Throughout this post I’ll use hibernate and spring as “given”, although they may be changed with any DI framework and any ORM or other persistence mechanism that relies on objects.
The accepted way to implement DDD with spring and hibernate is:
- use
@Configurable
to make the domain objects eligible for dependency injection (they are not instantiated by spring, so they need this special approach) - inject repository objects in the domain objects in order to allow the domain objects to do persistence-related operations
- use a thin, stateless, transactional service layer (a facade) to coordinate the domain objects
Implementation and description of this approach is shown in this extensive article. Another example (without spring) is http://dddsample.sourceforge.net/. I’ll discuss both later.
The alternative to this approach is the anemic domain model. It is considered an anti-pattern, but at the same time is very common and often used. The features of the anemic data model are simple – the domain objects have no business logic inside them – they are only data holders. Instead, the business logic is placed in services.
The reason this is considered an anti-pattern is because, first, this seems like a procedural approach. It is breaking the encapsulation, because the internal state of the object is, well, not internal at all. Second, as the domain object is the center of the design, it is harder to change it if its operations don’t belong to it, but to a number of stateless service classes instead. And domain-driven design is aimed at medium-to-large scale applications, which change a lot and need an easy way to make changes fast, without breaking other functionality. Thus it is important to have all the functionality of the object within the object itself. This also makes sure that there is less duplication of code.
So, instead of having a service calculate the price: ProductServiceImpl.calculatePrice(complexProduct)
we should simply have ComplexProduct.calculatePrice()
. And so whenever the domain experts say that the price calculation mechanism changes, the place to change it is exactly one and is the most straightforward one.
When simple operations are regarded, this looks easy. However, when one domain objects needs another domain object to do its job, it becomes more complicated. With the anemic data model this is achieved by simply injecting another Service into the current one and calling its methods. With the proposed DDD it is achieved by passing domain objects as arguments.
In my view, the domain object, which is also the hibernate entity, has its dependencies already set. But not by spring, because spring can’t know which exactly domain object to inject. They are “injected” by hibernate, because it knows exactly which (identified by primary key) domain object should be placed in another domain object. So, a little odd example – if a Product
has rotten and has to dispense smell in the warehouse, it has to call, for example, warehouse.increaseSmellLevel(getSmellCoeficient())
. And it has its precise Warehouse
without any interference from spring.
Now, here comes another point where I disagree. Most sources (including the two linked above) state that repositories / DAOs should be injected in the domain objects. No, they shouldn’t. Simply calling “save” or “update” doesn’t require knowledge of the internal state of the object. Hibernate knows everything anyway. So we are just passing the whole object to the repository.
Let’s split this in two – business logic and infrastructure logic. The domain object should not know anything of the infrastructure. That might mean that it should not know it is being saved somewhere. Does a product care of how it is stored ? No – it’s the storage mechanism that’s “interested”. And here are the practical disadvantages:
- CRUD is implemented by simply wrapping repository calls in all domain objects – duplication of code
- the domain object is transitively dependent on persistence – i.e. it is not a pure domain object, and if repositories change, it has to be changed as well. And in theory it should be changed only when the domain rules and attributes change
- people will be tempted to include transaction, caching and other logic inside the domain object
I’ll open a bracket here about a proposed solution in one of the above articles for making duplication of code, and boilerplate code easier to handle. Code generation is suggested. And I think code-generation is a sin. It moves the inability to get rid of duplicated or very similar code and abstract it, to tools. The most striking example is generating ProductDAO, CategoryDAO, WarehouseDAO, etc, etc. Generated code is hard to manage, cannot be extended and relies heavily on external metadata, which is definitely not an object-oriented approach.
Speaking of the repository, in the proposed examples each domain object should have a repository, which in turn will call the persistence mechanism. What do we get then:
User presses “save” in the UI > UI calls save on the service (in order to have transaction support) > Service calls save on the domain object > domain object calls save on the repository > the repository calls save on the persistence mechanism > the persistence mechanism saves the object.
Is it just me, or calling the domain object is redundant here. It is a pass-through method that adds nothing. And since a lot of functionality is related to CRUD (yes, even in big business applications), this looks quite bad to me.
And finally, I find the @Configurable
approach a hack. It does some magic in the background, which isn’t anything of the common language features (and is not a design pattern), and in order to understand how it happens you need a great deal of experience.
So, to summarize the big mess above
- domain objects should not be spring (IoC) managed, they should not have DAOs or anything related to infrastructure injected in them
- domain objects have the domain objects they depend on set by hibernate (or the persistence mechanism)
- domain objects perform the business logic, as the core idea of DDD is, but this does not include database queries or CRUD – only operations on the internal state of the object
- there is rarely need of DTOs – the domain objects are the DTOs themselves in most cases (which saves some boilerplate code)
- services perform CRUD operations, send emails, coordinate the domain objects, generate reports based on multiple domain objects, execute queries, etc.
- the service (application) layer isn’t that thin, but doesn’t include business rules that are intrinsic to the domain objects
- code generation should be avoided. Abstraction, design patterns and DI should be used to overcome the need of code generation, and ultimately – to get rid of code duplication.
=================================================
一个有见地的回复:
For one I think that technical issues should be separated from the functional domain. That means that domain objects should not be polluted by (technical) annotations or persistence methods (seperation of concerns). Dependency Injection (technical) should be outside the core domain. Persistence methods should be in a repository gateway, which (at least the interface, functional) is part of the domain. You know that you have a repository (functiuonal, within the domain), but not the way it’s implemented (technical). That’s outside the domain.
Just as the services (functionality) which calls them are part of the domain. These services are called from the user interface or other components outside this particular domain.
Transactional boundaries and session management is not domain functionality and should be outside the domain in a separate (thin) layer, which calls the services or domain objects within the domain.
DTO’s are useful for communicating outside the domain. In simple applications (with session management in the UI) the DO’s can be used, but I would not recommend it. I prefer the use of DTO’s which are assembled within the transaction boundary, and can be used outside the transaction (and session) boundary as to avoid potential LazyInitializationExceptions, which I am, unfortunately, very familiar with.
Dependency injection should not be in Domain Objects. If you want to use something like the AgeCalculator mentioned above, just call the user.getAge() method wich Alex suggested.
This method contains something like
class User {
public int getAge() {
AgeFactory.getInstance().getAgeCalculator().calculateAge(this);
}
}
The AgeFactory is a singleton, which has the correct calculator injected. You can use Spring for that.
class AgeFactory {
AgeFactory instance = new AgeFactory();
AgeCalculator ageCalculator = null;
public AgeFactory getInstance() {
return instance;
}
public void setAgeCalculator(AgeCalculator ageCalculator) {
this.ageCalculator = ageCalculator;
}
public AgeCalculator getAgeCalculator() {
return this.ageCalculator;
}
}
The implementation of the repository gateway objects can be done in the same way.
Usage of the repository (in a service) can be something like this
class ServiceImpl implements Service {
public void save(User user) {
RepositoryFactory.getInstance().getUserRepository().save(user);
}
}
These factories must be within the domain, because the domein objects must use them. But the dependency injection of these factories can be done outside the core domain (if you don’t use annotations). This way the core functional domain is not polluted with the technical issues of dependency injection.
Finally about annotations, I hate them, because it’s a magic black box. I rather use xml configuration files because they are probably easier to understand. Furthermore without annotations you can have the configuration files outside the core domain, and can use mock objects which are created and set on the factory without dependency injection within testcases. You can even choose to use dependency injection within part of the unit tests and without DI in other unit tests.