可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
We are following Domain Driven Design for the implementation of a large website.
However by putting the behaviour on the domain objects we are ending up with some very large classes.
For example on our WebsiteUser object, we have many many methods - e.g. dealing with passwords, order history, refunds, customer segmentation. All of these methods are directly related to the user. Many of these methods delegate internally to other child object but
this still results in some very large classes.
I'm keen to avoid exposing lots of child objects
e.g. user.getOrderHistory().getLatestOrder().
What other strategies can be used to avoid this problems?
回答1:
The issues you are seeing aren't caused by Domain Driven Design, but rather by a lack of separation of concerns. Domain Driven Design isn't just about placing data and behavior together.
The first thing I would recommend is taking a day or so and reading Domain Driven Design Quickly available as a free download from Info-Q. This will provide an overview of the different types of domain objects: entities, value objects, services, repositories, and factories.
The second thing I would recommend is to go read up on the Single Responsibility Principle.
The third thing I would recommend is that you begin to immerse yourself in Test Driven Development. While learning to design by writing tests first won't necessarily make you designs great, they tend to guide you toward loosely coupled designs and reveal design issues earlier.
In the example you provided, WebsiteUser definitely has way too many responsibilities. In fact, you may not have a need for WebsiteUser
at all as users are generally represented by an ISecurityPrincipal
.
It's a bit hard to suggest exactly how you should approach your design given the lack of business context, but I would first recommend doing some brain-storming by creating some index cards representing each of the major nouns you have in your system (e.g. Customer, Order, Receipt, Product, etc.). Write down candidate class names at the top, what responsibilities you feel are inherent to the class off to the left, and the classes it will collaborate with to the right. If some behavior doesn't feel like it belongs on any of the objects, it's probably a good service candidate (i.e. AuthenticationService). Spread the cards out on the table with your colleges and discuss. Don't make too much of this though, as this is really only intended as a brainstorming design exercise. It can be a little easier to do this at times than using a whiteboard because you can move things around.
Long term, you should really pick up the book Domain Driven Design by Eric Evans. It's a big read, but well worth your time. I'd also recommend you pick up either
Agile Software Development, Principles, Patterns, and Practices or Agile Principles, Patterns, and Practices in C# depending on your language preference.
回答2:
Although real humans have lots of responsibilities, you're heading towards the God object anti-pattern.
As others have hinted, you should extract those responsibilities into separate Repositories and/or Domain Services. E.g.:
SecurityService.Authenticate(credentials, customer)
OrderRepository.GetOrderHistoryFor(Customer)
RefundsService.StartRefundProcess(order)
Be specific with naming conventions (i.e. use OrderRepository or OrderService, instead of OrderManager)
You've run into this problem because of convenience. i.e. it's convenient to treat a WebsiteUser
as an aggregate root, and to access everything through it.
If you place more emphasis on clarity instead of convenience, it should help separate these concerns. Unfortunately, it does mean that team members must now be aware of the new Services.
Another way to think of it: just as Entities shouldn't perform their own persistence (which is why we use Repositories), your WebsiteUser
should not handle Refunds/Segmentation/etc.
Hope that helps!
回答3:
I ran into the same problem, and I found that using child "manager" objects was the best solution in our case.
For example, in your case, you might have:
User u = ...;
OrderHistoryManager histMan = user.getOrderHistoryManager();
Then you can use the histMan for anything you want. Obviously you thought of this, but I don't know why you want to avoid it. It seperates concerns when you have objects which seem to do too much.
Think about it this way. If you had a "Human" object, and you had to implement the chew()
method. Would you put it on the Human
object or the Mouth
child object.
回答4:
A very simple rule of thumb to follow is "most of the methods in your class HAVE to use most of the instance variables in your class" - if you follow this rule the classes will be automatically of the right size.
回答5:
You may want to consider inversing some things. For example, a Customer doesn't need to have an Order property (or a history of orders) - you can leave those out of the Customer class. So instead of
public void doSomethingWithOrders(Customer customer, Calendar from, Calendar to) {
List = customer.getOrders(from, to);
for (Order order : orders) {
order.doSomething();
}
}
you could instead do:
public void doSomethingWithOrders(Customer customer, Calendar from, Calendar to) {
List = orderService.getOrders(customer, from, to);
for (Order order : orders) {
order.doSomething();
}
}
This is 'looser' coupling, but still you can get all the orders belonging to a customer. I'm sure there's smarter people than me that have the right names and links referring to the above.
回答6:
I believe that your problem is actually related to Bounded Contexts. For what I see, "dealing with passwords, order history, refunds, customer segmentation", each one of these can be a bounded context. Therefore, you might consider splitting your WebsiteUser into multiple entities, each one corresponding to a context. There may arise some duplication, but you gain focus on your domain and get rid off very large classes with multiple responsibilities.