0

During a conversation with our Domain Expert we can across this feature:

A Customer service agent can cancel an Order by decreasing its quantity. To cancel an Order we decrease its quantity by the specified quantity, change its status to either partially cancelled or fully cancelled and log the CS agent who issued the cancellation.

We are currently split between several solutions (assuming we have the agentId, orderId and cancelledQty from the request):

Solution 1:

CSAgent agent= (CSAgent) EmployeeRepository.getById(agentId);
Order order= OrderRepository.getById(orderId);
agent.cancel(order,cancelledQty);

vs
Solution 2:

CSAgent agent= (CSAgent) EmployeeRepository.getById(agentId);
Order order= OrderRepository.getById(orderId);
order.cancel(cancelledQty,agent);

Solution 1 really captures what the requirements is conveying and reads really well; however, solution 2 seems more natural when coding the implementation of the cancel() method as I'll mostly update fields in the Order class itself which I hope to keep private with no public setters.
The only thing I need from the CSAgnet class is a snapshot of its data to be stored with the Order.

So how should I determine the best course of action from a DDD perspective?

2
  • "log the CS agent who issued the cancellation" What kind of logging is that? Quite rare to have logging as part of the domain. Is this only instance of something being logged in domain? – Euphoric Aug 10 '14 at 17:54
  • @Euphoric the term log here might be overloaded. What he meant is we need to track who issued the cancellation order. – Songo Aug 10 '14 at 18:50
5

Your Solution 2 is the better option because of single responsibility and encapsulation concerns.

CSAgent agent= (CSAgent) EmployeeRepository.getById(agentId);
Order order= OrderRepository.getById(orderId);
order.cancel(cancelledQty,agent);

While an Agent invokes the cancel(), a cancellation is meaningless outside the context of an oder. What you have described is a classic example of an Actor acting upon an Object, invoking a method of the Object.

Putting the 'cancel()method withinCSAgentmoves theCSAgent` object a little closer to becoming a "god object."

Your additional comment of:

The only thing I need from the CSAgnet class is a snapshot of its data to be stored with the Order.

reinforces why the responsibility of cancelling an order belongs to the Order itself.

Your additional or potential requirement of a partial cancellation adds to the justification for the second solution. A partial cancellation will likely have additional logic beyond a complete cancellation. That's business logic that belongs to the Order and is something that the CSAgent should be blissfully ignorant of.

2

Solution 1 violates Single Responsibility Principle

  • CSAgent.cancel sounds like we're cancelling an agent!
  • The method parameters hints that the agent knows order implimentation enough to set the order's state correctly. CSAgent certainly has to know the difference between "partial" and "complete" order processing.
  • Even if CSAgent.cancel(order, quantity) just calls order.cancel(quantity), CSAgent simply has no business doing the order cancel process for another entity.

Solution 2 violates Single Responsibility Principle

  • Just switch CSAgent with Order above. It's the same thing.

We need a God Object

  • Well, not exactly; but a class that executes the order cancellation process. That logging is missing from all our solutions tells me this.
  • "OrderCancelProcessor" (just to give it a name for the time being)

  • Both CSAgent and Order may have CancelOrder() methods but they will not take references to each other. They only update their own state.

  • May process differently depending on "complete" vs "partial" cancellation.

Formalize Cancellation - including Partial and Complete

  • I think a OrderCancellation entity would naturally, eventually come out of the DDD process. This might represent an immutable order cancellation request (state) - a DDD thing.

  • I suspect this would be passed to agent.cancel() and order.cancel().

  • Likewise, It's clear there are different aspects of "cancellation". I suspect that the OrderCancellation may encapsulate the "complete" and "partial" concepts. As such it communicates to the players in the cancellation workflow that this cancellation is partial or complete.

  • Even if the difference (partial vs. complete cancellation) is literally only a matter of Order.Quantity being zero or not; it is a mistake to ignore domain concepts just because underlying code may be trivial.

  • If you don't encapsulate these concepts I expect classes will be infested with this:

    • if(thisOrder.Quantity > 0) { ... } else { ...}
    • I guarantee some/many/most/all of the cancer-code above will not check for negative numbers.
    • This cancer-code becomes a poster child for DRY and SRP
  • Each class involved in the order cancel workflow does its own thing in terms of a order cancellation vice an order quantity.

Getting Process Players Together

  • The Command Design Pattern comes to mind. This design will keep the CSAgent and Order decoupled and help us adhere to SRP, DRY, etc.

  • NOTE: Command Pattern may be overkill. It all depends on the domain model specifics which I am blissfully ignorant of.

2
  • 1
    I believe that, instead of "God Object", you might mean method object (as in an object extracted to encapsulate the logic of a method). In addition, you could generalize the OrderCancelProcessor into an OrderProcessor to handle other order operations, which starts to look a little like the Repository pattern. – cbojar Aug 10 '14 at 17:55
  • @cbojar; "OrderProcessor" - fully agree. As for "god object", that was not meant to be technically accurate. – radarbob Aug 10 '14 at 19:11
1

If agent changes state then a combination is required. If not, then Solution 2 with the following twist:

order.cancelByAgent(cancelledQty,agent.snapshot);

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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