6

Context

I'm developing an application using a Domain Driven Design approach. I want to use a design pattern wherever appropriate and apply all SOLID principles.

Scenario

I have an order and I want to allow clients to add order lines to it.

Consider the following pseudo code:

class Order
    OrderLines[] (readonly)
    AddOrderLine(orderLine)

class OrderLine
    Product
    Amount

class OrderLineFactory
    OrderLine Create(product, amount)

class OrderLineRepository
    Save(orderLine)

Now lets add an OrderLine to the Order:

Order.AddOrderLine(OrderLineFactory.Create(product: "Chocolate Cake", amount: 3))

By the way, the OrderLineFactory is there because in my case, there is some relatively complex logic involved with creating order lines. This is one of the main reasons for applying the factory pattern.

Problem

The problem is that I could do this instead...

orderLine = OrderLineFactory.Create(product: "Chocolate Cake", amount: 3)
OrderLineRepository.Save(orderLine)

...and get around the Order.AddOrderLine method, effectively short-cutting any checks that are performed in that method (e.g. protecting against duplicates or limiting the order amount).

Working towards a solution

An argument could be that there shouldn't be a Save method on OrderLineRepository, because an OrderLine isn't an aggregate root. This would (just like I want) render the client incapable of storing the bare OrderLine except from adding it to the Order and saving the Order consequently.

However, having a Save method on repositories that concern non-root aggregates, allows for saving changes made to them directly, such as:

orderLine = orderLineRepository.Get(31923)
orderLine.Amount = 5
OrderLineRepository.Save(orderLine)

Not being able to save the OrderLine would require me to do something like...

order = OrderRepository.FindByOrderLineId(31923)
order.OrderLines[31923].Amount = 5
OrderRepository.Save(order)

...which isn't all that efficient or practical. I could add a requirement to provide the (actually redundant) Order ID as well but that would put some unnecessary burden on the client.

So lets say I want to stick with my OrderLineRepository.Save method and still find a way to stop my client from being able to get around my Order.AddOrderLine method.

All I can think of now, is dropping the OrderLineFactory and creating a domain service that combines the creation and the assignment.

class OrderService
    AddOrderLine(order, product, amount)

So we can:

order = orderRepository.Get(123)
OrderService.AddOrderLine(order: order, product: "Chocolate Cake", amount: 3))
orderRepository.Save(order)

I don't really like the service combining multiple concerns here though, those of creation and assignment. That would call for... a factory... again.

Okay, let's consider a private factory then, one that's not available to the client but to the domain service alone. In my case I would get stuck with unit testing and Inversion Of Control. I'd be creating an uninjectable dependency, since my client wouldn't be able to register my non-public factory.

Summary

  • I want my client to be able to create order lines through the Order class and in no other way
  • I don't want to mangle multiple concerns in a single class
  • I want to keep OrderLine creation outside of the Order class
  • I'd like to keep using a factory for that

So I'm a little stuck here, trying to find the best way to deal with this. Any suggestions?

2
  • You really are asking for an atomic createAndSave message. Would it help if you think of that object as a factory that has some saving functionality munged in? Commented Feb 25, 2012 at 15:41
  • I think that leads to the same problem as creating the OrderService: it combines multiple concerns in one class. I'm really trying to stick with the separation of concerns/single responsibility principle (see en.wikipedia.org/wiki/Single_responsibility_principle). Commented Feb 25, 2012 at 16:02

2 Answers 2

4

I feel like you were really close, but just missed it

order = orderRepository.Get(123)
OrderService.AddOrderLine(order: order, product: "Chocolate Cake", amount: 3))
orderRepository.Save(order)

What you were looking for is more like this:

order = orderRepository.Get(123)
order.AddOrderLine(orderService: orderService, product: "Chocolate Cake", amount: 3))
orderRepository.Save(order)

Domain services support queries within the domain model - they don't write changes to the model; the aggregates change and protect the domain state. So as a rule, you want to pass the domain service to the aggregate, then let the aggregate pass current state to the service, as needed. For instance

// Order.AddOrderLine()
orderLine = orderService.createOrderLine(this.id, product, amount);
this.lines.add(orderLine);

Note the (implied) separation of responsibilities

  • The domain service creates an instance of the orderLine, using only the context provided to it. But there's no persistence here, it's just transient data in memory at this point.
  • The aggregate then evaluates the orderLine to determine whether or not it satisfies the invariant. If it does, the orderLine will become part of the graph of entities reachable via the aggregate root.

However, having a Save method on repositories that concern non-root aggregates, allows for saving changes made to them directly

That's not a good thing?

orderLine = orderLineRepository.Get(31923)
orderLine.Amount = 5
OrderLineRepository.Save(orderLine)

But if this orderLine is part of an order aggregate, then presumably the Order is expected to check that the state of the order is consistent. It can't do that if you insist on being able to mutate the subordinate entities directly.

Part of the point to aggregates is that all changes to the domain model must go through paths that force the model to remain consistent. Are all of the OrderLine methods going to walk back up the graph to the Order to ensure that the invariant is still satisfied?

Of course, this might actually be a hint that the aggregate boundaries are in the wrong place. If you should be able to modify OrderLine without the entire Order, then perhaps OrderLines are aggregate roots, and not merely subordinate entities. Your domain experts might tell you that discrepancies between the Order and OrderLines aren't actually particularly expensive; ensuring that they are rare (rather than eliminating them entirely) may suffice. Horses for courses.

3

Why not make the order responsible for creating orderlines?

The AddOrderline function could take the Product and Amount parameters and create an Orderline changing an orderline would be by product on an order so you would have a function ChangeOrderLine(string productName, int amount) and DeleteOrderLine(string productName)

Done and done.

7
  • I think you are right for simple cases. In this case, let's just say that creating my order line involves some relatively complex logic (I'll update the question with this). Since (less-simple) object creation is a separate concern, this is a valid reason to use the factory pattern. Commented Feb 25, 2012 at 16:38
  • 2
    There's no reason the Order can't use a factory, if a factory is required. The responsibility for adding the OrderLine to the Order still lies in the Order object.
    – Eric King
    Commented Feb 25, 2012 at 19:37
  • DDD purists often strongly advice against having entities depend on services and I agree with them. Without the dependencies, the domain can stay pure and not become infected with concerns that really belong elsewhere (for example, in domain services or application services such as command handlers). Commented Feb 25, 2012 at 22:44
  • In my opinion, it is important to keep the domain model only concerned with expressing the domain and defining entity interaction (therefore not being anemic). Commented Feb 25, 2012 at 22:52
  • Maybe a nitpick, but the factory in this context is not a service. If the only entity that should be charged with creating a new orderline is an order, then that's where the code should be.
    – Eric King
    Commented Feb 25, 2012 at 23:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.