1
votes

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?

1

1 Answers

0
votes

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.