1

After reading DDD - Modifications of child objects within aggregate and Update an entity inside an aggregate I still puzzled with the implementation of entity changes within an aggregate. For what I understand the aggregate root speaks for the whole (or entire aggregate) and delegates 'commands' changes down to the rest.

This last part, the delegating down to the rest is causing some problems. In the example below I want to change the quantity of a particular orderline. I'm addressing the root 'Order' and telling it to change the quantity of a orderline identified by a local identifier.

When all business rules are met an event can be created and applied on the aggregate. For now all the events are applied on the aggregate root, and I think that is a good practices, so all the commands are directed on the root and this changes the state of the aggregate. Also the aggregate root is the only one creating events, letting the world know what happened.

class Order extends AggregateRoot
{
    private $orderLines = [];
    public function changeOrderLineQuantity(string $id, int $quantity)
    {
        if ($quantity < 0) {
            throw new \Exception("Quantity may not be lower than zero.");
        }

        $this->applyChange(new OrderLineQuantityChangedEvent(
            $id, $quantity
        ));
    }

    private function onOrderLineQuantityChangedEvent(OrderLineQuantityChangedEvent $event)
    {
        $orderLine = $this->orderLines[$event->getId()];

        $orderLine->changeQuantity($event->getQuantity());
    }
}

class OrderLine extends Entity
{
    private $quantity = 0;

    public function changeQuantity(int $quantity)
    {
        if ($quantity < 0) {
            throw new \Exception("Quantity may not be lower than zero.");
        }

        $this->quantity = $quantity;
    }
}

But, when I am applying this implementation I have a problem, as you notice the business rule for checking the value of $quantity is located in two classes. This is on purpose, because I don't really know the best spot. The rule is only applied within the OrderLine class, thus it doesn't belong in Order. But when I'm removing this from Order events will be created that cannot be applied, because not all business rules are met. This is also something that is not wanted.

I can created a method in the class OrderLine like:

    public function canChangeQuantity(int $quantity)
    {
        if ($quantity < 0) {
            return false;
        }
        return true;
    }

changing the method in the OrderLine to:

    public function changeQuantity(int $quantity)
    {
        if ($this->canChangeQuantity($quantity) < 0) {
            throw new \Exception("Quantity may not be lower than zero.");
        }

        $this->quantity = $quantity;
    }

Now I can alter the method within the Order class to:

    public function changeOrderLineQuantity(string $id, int $quantity)
    {
        $orderLine = $this->orderLines[$event->getId()];
        if ($orderLine->canChangeQuantity($quantity)) {
            throw new \Exception("Quantity may not be lower than zero.");
        }

        $this->applyChange(new OrderLineQuantityChangedEvent(
            $id, $quantity
        ));
    }

Ensuring the business logic is where it belongs and also not in two places. This is an option, but if the complexity increases and the model becomes larger I can imagine that these practices become more complex.

For now I have to questions: (1) How do you cope with alterations deep within the aggregate that are started from the root? (2) When the business rules increase (e.g, max quantity is 10, but on Monday 3 more, and for product X max is 3 items). Is it good practices to supply each command / method on the aggregate root a domain services that is validating these business rules?

Pakspul
  • 648
  • 5
  • 17

1 Answers1

1

I have a problem, as you notice the business rule for checking the value of $quantity is located in two classes.

From an "object oriented" perspective, Order::changeOrderLineQuantity($id, $quantity) is a message. It is normal for messages to have schema, and for schema to restrict the range of values that are permitted in any given field.

So this code here:

public function changeOrderLineQuantity(string $id, int $quantity)
{
    if ($quantity < 0) {
        throw new \Exception("Quantity may not be lower than zero.");
    }

is an example of message validation, you are checking to see that quantity is in the allowed range of values because the general-purpose int primitive is too permissive.

What domain modelers using strongly typed languages will often do here is introduce a new type, aka a ValueObject, that models the data with its range restrictions.

// Disclaimer: PHP is not my first language
class Quantity {
    public function __construct(int $quantity) {
        if ($quantity < 0) {
            throw new \Exception("Quantity may not be lower than zero.");
        }
        $this.quantity = quantity
    }
    //  ...
}

In the ease cases, Quantity, as understood by Orders::changeOrderLineQuantity(...) is the same domain concept as Quantity as understood by OrderLineQuantityChangedEvent(...) is the same domain concept as Quantity as understood by OrderLine::changeQuantity(...), and therefore you can re-use the same type everywhere; the type checker therefore ensures that the correct constraints are satisfied.

Edit

As noted by Eben Roux in the comments to this question, Quantity here should not be understood to be some universal, general-purpose type. It is instead specific to the context of Orders and OrderLines, and other parts of the code that share the same constraints for the same reason.

A complete solution might have several different Quantity types in different namespaces.

VoiceOfUnreason
  • 52,766
  • 5
  • 49
  • 91
  • How do you account for changes in business rules over time. As you construct or deserialize ValueTypes that are implemented this way, are you applying business rules? If so, what do you do if a change in a business rule means that existing data is no longer "valid"? – CPerson Dec 30 '19 at 21:24
  • That's a very good question. The short version is that you need to do some careful thinking about the difference between "unrepresentable" states and "unreachable" states. Unreachable is "easy", because that's entirely up to the domain model. "Unrepresentable" is manageable if all of the impacted data is already clean. For the rest? "it depends" - you start doing cost vs benefit analysis on your options. – VoiceOfUnreason Dec 30 '19 at 22:20
  • Thank you for your explanation! I have used value objects on other points, but I did see this part clearly. I have tried to create an example so close to my situation so I can copy the concepts, but is this also allowed with entities? I have a task with comments and a user can start a task with or without a comment. The method I currently have is start(VOUser $user, string $comment) inside the method I combine these in a entity, but is also allowed to already supply a entity Comment to the start method? – Pakspul Dec 31 '19 at 06:46
  • I wouldn't - messages are normally immutable, and entities are normally mutable, so there's a bit of conflict when you include a mutable entity within a message. But that's an argument from general principles - the pattern may be "fine" in appropriate circumstances. – VoiceOfUnreason Dec 31 '19 at 17:32
  • I agree with the broad strokes here. I'll just add that checking the same business rule in multiple places is probably not the best idea. You could forego the check in `changeOrderLineQuantity` as the event processing would pick it up since the invariant here belongs to the `OrderLine`. Generic value objects may make things easier although a quantity may very well be a `decimal` and some quantities may be required to be negative and some perhaps may not be 0. It depends on the use. But the general idea stands none-the-less. – Eben Roux Jan 01 '20 at 09:05