Image may be NSFW.
Clik here to view. Update: The new and improved solution is now available: Domain Events, Take 2.
Most people getting started with DDD and the Domain Model pattern get stuck on this. For a while I tried answering this on the discussion groups, but here we have a nice example that I can point to next time.
The underlying problem I’ve noticed over the past few years is that developers are still thinking in terms of querying when they need more data. When moving to the Domain Model pattern, you have to “simply” represent the domain concepts in code – in other words, see things you aren’t used to seeing. I’ll highlight that part in the question below so that you can see where I’m going to go with this in my answer:
I have an instance where I believe I need access to a service or repository from my entity to evaluate a business rule but I’m using NHibernate for persistence so I don’t have a real good way to inject services into my entity. Can I get some viewpoints on just passing the services to my entity vs. using a facade?
Let me explain my problem to provide more context to the problem.
The core domain revolves around renting video games. I am working on a new feature to allow customers to trade in old video games. Customers can trade in multiple games at a time so we have a TradeInCart entity that works similar to most shopping carts that everybody is familiar with. However there are several rules that limit the items that can be placed into the TradeInCart. The core rules are:
1. Only 3 games of the same title can be added to the cart.
2. The total number of items in the cart cannot exceed 10.
3. No games can be added to the cart that the customer had previously reported lost with regards to their rental membership.
a. If an attempt is made to add a previously reported lost game, then we need to log a BadQueueStatusAddAttempt to the persistence store.So the first 2 rules are easily handled internally by the cart through an Add operation. Sample cart interface is below.
1: class TradeInCart{2: Account Account{get;}
3: LineItem Add(Game game);
4: ValidationResult CanAdd(Game game);
5: IList<LineItems> LineItems{get;}
6: }
However the #3 rule is much more complicated and can’t be handled internally by the cart, so I have to depend on external services. Splitting up the validation logic for a cart add operation doesn’t seem very appealing to me at all. So I have the option of passing in a repository to get the previously reported lost games and a service to log bad attempts. This makes my cart interface ugly real quick.
1: class TradeInCart{2: Account Account{get;}
3: LineItem Add(
4: Game game,
5: IRepository<QueueHistory> repository,
6: LoggingService service);
7:
8: ValidationResult CanAdd(
9: Game game,
10: IRepository<QueueHistory> repository,
11: LoggingService service);
12:
13: IList<LineItems> LineItems{get;}
14: }
The alternative option is to have a TradeInCartFacade that handles the validations and adding the items to the cart. The façade can have the repository and services injected though DI which is nice, but the big negative is that the cart ends up totally anemic.
Any thought on this would be greatly appreciated.
Thanks,
Jesse
As I highlighted above, the thing that will help you with your business rules is to introduce the Customer object (that you probably already have) with the property GamesReportedLost (an IList<Game>). Your TradeInCart would have a reference to the Customer object and could then check the rule in the Add method.
Before I go into the code, it looks like your Account object might be used the same way, but your description of the domain doesn’t mention accounts, so I’m going to assume that that’s unrelated for now:
1: public class Customer{
2:
3: /* other properties and methods */
4:
5: private IList<Game> gamesReportedLost;
6: public virtual IList<Game> GamesReportedLost
7: {
8: get
9: {
10: return gamesReportedLost;
11: }
12: set
13: {
14: gamesReportedLost = value;
15: }
16: }
17: }
Keep in mind that the GamesReportedLost is a persistent property of Customer. Every time a customer reports a game lost, this list needs to be kept up to date. Here’s the TradeInCart now:
1: public class TradeInCart
2: {
3: /* other properties and methods */
4:
5: private Customer customer;
6: public virtual Customer Customer
7: {
8: get { return customer; }
9: set { customer = value; }
10: }
11:
12: private IList<LineItem> lineItems;
13: public virtual IList<LineItem> LineItems
14: {
15: get { return lineItems; }
16: set { lineItems = value; }
17: }
18:
19: public void Add(Game game)
20: {
21: if (lineItems.Count >= CONSTANTS.MaxItemsPerCart)
22: {
23: FailureEvents.RaiseCartIsFullEvent();
24: return;
25: }
26:
27: if (NumberOfGameAlreadyInCart(game) >=
28: CONSTANTS.MaxNumberOfSameGamePerCart)
29: {
30: FailureEvents
31: .RaiseMaxNumberOfSameGamePerCartReachedEvent();
32: return;
33: }
34:
35: if (customer.GamesReportedLost.Contains(game))
36: FailureEvents.RaiseGameReportedLostEvent();
37: else
38: this.lineItems.Add(new LineItem(game));
39: }
40:
41: private int NumberOfGameAlreadyInCart(Game game)
42: {
43: int result = 0;
44:
45: foreach(LineItem li in this.lineItems)
46: if (li.Game == game)
47: result++;
48:
49: return result;
50: }
51: }
52:
53: public static class FailureEvents
54: {
55: public static event EventHandler GameReportedLost;
56: public static void RaiseGameReportedLostEvent()
57: {
58: if (GameReportedLost != null)
59: GameReportedLost(null, null);
60: }
61:
62: public static event EventHandler CartIsFull;
63: public static void RaiseCartIsFullEvent()
64: {
65: if (CartIsFull != null)
66: CartIsFull(null, null);
67: }
68:
69: public static event EventHandler MaxNumberOfSameGamePerCartReached;
70: public static void RaiseMaxNumberOfSameGamePerCartReachedEvent()
71: {
72: if (MaxNumberOfSameGamePerCartReached != null)
73: MaxNumberOfSameGamePerCartReached(null, null);
74: }
75: }
Image may be NSFW.
Clik here to view. Your service layer class that calls the Add method of TradeInCart would first subscribe to the relevant events in FailureEvents. If one of those events is raised, it would do the necessary logging, external system calls, etc.
As you can see, the API of TradeInCart doesn’t need to make use of any external repositories, nor do you need to inject any other external dependencies in.
One thing I didn’t do in the above code to keep it “short” is to define the relevant custom EventArgs for bubbling up the information as to which game was reported lost or already have 3 of those in the cart. That is something that definitely should be done so that the service layer can pass this information back to the client.
Here’s a look at Service Layer code:
1: public class AddGameToCartMessageHandler :
2: BaseMessageHandler<AddGameToCartMessage>
3: {
4: public override void Handle(AddGameToCartMessage m)
5: {
6: using (ISession session = SessionFactory.OpenSession())
7: using (ITransaction tx = session.BeginTransaction())
8: {
9: TradeInCart cart = session.Get<TradeInCart>(m.CartId);
10: Game g = session.Get<Game>(m.GameId);
11:
12: Domain.FailureEvents.GameReportedLost +=
13: gameReportedLost;
14: Domain.FailureEvents.CartIsFull +=
15: cartIsFull;
16: Domain.FailureEvents.MaxNumberOfSameGamePerCartReached +=
17: maxNumberOfSameGamePerCartReached;
18:
19: cart.Add(g);
20:
21: Domain.FailureEvents.GameReportedLost -=
22: gameReportedLost;
23: Domain.FailureEvents.CartIsFull -=
24: cartIsFull;
25: Domain.FailureEvents.MaxNumberOfSameGamePerCartReached -=
26: maxNumberOfSameGamePerCartReached;
27:
28: tx.Commit();
29: }
30: }
31:
32: private EventHandler gameReportedLost = delegate {
33: Bus.Return((int)ErrorCodes.GameReportedLost);
34: };
35:
36: private EventHandler cartIsFull = delegate {
37: Bus.Return((int)ErrorCodes.CartIsFull);
38: };
39:
40: private EventHandler maxNumberOfSameGamePerCartReached = delegate {
41: Bus.Return((int)ErrorCodes.MaxNumberOfSameGamePerCartReached);
42: };
43: }
44: }
Image may be NSFW.
Clik here to view.It’s important to remember to clean up your event subscriptions so that your Service Layer objects get garbage collected. This is one of the primary causes of memory leaks when using static events in your Domain Model. I’m hoping to find ways to use lambdas to decrease this repetitive coding pattern. You might be thinking to yourself that non-static events on your Domain Model objects would be easier, since those objects would get collected, freeing up the service layer objects for collection as well. There’s just on small problem:
The problem is that if an event is raised by a child (or grandchild object), the service layer object may not even know that that grandchild was involved and, as such, would not have subscribed to that event. The only way the service layer could work was by knowing how the Domain Model worked internally – in essence, breaking encapsulation.
If you’re thinking that using exceptions would be better, you’d be right in thinking that that won’t break encapsulation, and that you wouldn’t need all that subscribe/unsubscribe code in the service layer. The only problem is that the Domain Model needs to know that the service layer had a default catch clause so that it wouldn’t blow up. Otherwise, the service layer (or WCF, or nServiceBus) may end up flagging that message as a poison message (Read more about poison messages). You’d also have to be extremely careful about in which environments you used your Domain Model – in other words, your reuse is shot.
Conclusion
I never said it would be easy Image may be NSFW.
Clik here to view.
However, the solution is simple (not complex). The same patterns occur over and over. The design is consistent. By focusing on the dependencies we now have a domain model that is reusable across many environments (server, client, sql clr, silverlight). The domain model is also testable without resorting to any fancy mock objects.
One closing comment – while I do my best to write code that is consistent with production quality environments, this code is more about demonstrating design principles. As such, I focus more on the self-documenting aspects of the code and have elided many production concerns.
Do you have a better solution?
Something that I haven’t considered?
Do me a favour – leave me a comment. Tell me what you think.