Question

Short format of question

Is it within best practices of DDD and OOP to inject services on entity method calls?

Long format example

Let's say we have the classic Order-LineItems case in DDD, where we have a Domain Entity called an Order, which also acts as the Aggregate Root, and that Entity is comprised not only of it's Value Objects, but also a collection of Line Item Entities.

Suppose we want fluent syntax in our application, so that we can do something like this (noting the syntax in line 2, where we call the getLineItems method):

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems($orderService) as $lineItem) {
  ...
}

We don't want to inject any sort of LineItemRepository into the OrderEntity, as that is a violation of several principles I can think of. But, the fluency of the syntax is something that we really want, because it's easy to read and maintain, as well as test.

Consider the following code, noting the method getLineItems in OrderEntity:

interface IOrderService {
    public function getOrderByID($orderID) : OrderEntity;
    public function getLineItems(OrderEntity $orderEntity) : LineItemCollection;
}

class OrderService implements IOrderService {
    private $orderRepository;
    private $lineItemRepository;

    public function __construct(IOrderRepository $orderRepository, ILineItemRepository $lineItemRepository) {
        $this->orderRepository = $orderRepository;
        $this->lineItemRepository = $lineItemRepository;
    }

    public function getOrderByID($orderID) : OrderEntity {
        return $this->orderRepository->getByID($orderID);
    }

    public function getLineItems(OrderEntity $orderEntity) : LineItemCollection {
        return $this->lineItemRepository->getLineItemsByOrderID($orderEntity->ID());
    }
}

class OrderEntity {
    private $ID;
    private $lineItems;

    public function getLineItems(IOrderServiceInternal $orderService) {
        if(!is_null($this->lineItems)) {
            $this->lineItems = $orderService->getLineItems($this);
        }
        return $this->lineItems;
    }
}

Is that the accepted way of implementing fluent syntax in Entities without violating core principles of DDD and OOP? To me it seems fine, as we're only exposing the service layer, not the infrastructure layer (that is nested within the service)

Was it helpful?

Solution

It's totally fine to pass a Domain service in an entity call. Say, we need to calculate an invoice sum with some complicated algorithm that can depend on, say, a customer type. Here is what it might look like:

class Invoice
{
    private $currency;
    private $customerId;

    public function __construct()
    {
    }

    public function sum(InvoiceCalculator $calculator)
    {
        $sum =
            new SumRecord(
                $calculator->calculate($this)
            )
        ;

        if ($sum->isZero()) {
            $this->events->add(new ZeroSumCalculated());
        }

        return $sum;
    }
}

Another approach though is to separate out a business-logic that is located in domain service via domain events. Keep in mind that this approach implies just different application services, but the same database transaction scope.

The third approach is the one I'm in favor of: if I find myself using a domain service, that probably means I missed some domain concept, since I model my concepts primarily with nouns, not verbs. So, ideally, I don't need a domain service at all and a good part of all my business-logic resides in decorators.

OTHER TIPS

Is it within best practices of DDD and OOP to inject services on entity method calls?

No, you should not inject anything inside your domain layer (this includes entities, value objects, factories and domain services). This layer should be agnostic of any framework, 3rd party libraries or technology and should not make any IO calls.

$order->getLineItems($orderService)

This is wrong as the Aggregate should not need anything else but itself to return the order items. The entire Aggregate should be already loaded before its method call. If you feel that this should be lazy loaded then are two possibilities:

  1. Your Aggregates boundaries are wrong, they are too large.

  2. In this usecase you use the Aggregate only for reading. The best solution is to split the write model from the read model (i.e. use CQRS). In this cleaner architecture you are not allowed to query the Aggregate but a read model.

I am shocked to read some of the answers here.

It's perfectly valid to pass domain services into entity methods in DDD to delegate some business calculations. As an example, imagine your aggregate root (an entity) needs to access an external resource through http in order to do some business logic and raise an event. If you don't inject the service through the entity's business method, how else you would do it? Would you instantiate an http client inside your entity? That sounds like a terrible idea.

What's incorrect is to inject services in aggregates through its constructor, but through a business method it's ok and perfectly normal.

The key idea in DDD tactical patterns: the application accesses all data in the application by acting on an aggregate root. This implies that the only entities which are accessible outside of the domain model are the aggregate roots.

The Order aggregate root would never yield a reference to its lineitem collection that would allow you to modify the collection, nor would it yield a collection of references to any line item that would allow you to modify it. If you want to change the Order aggregate, the hollywood principle applies: "Tell, don't ask".

Returning values from within the aggregate is fine, because values are inherently immutable; you can't change my data by changing your copy of it.

Using a domain service as an argument, to assist the aggregate in providing the correct values, is a perfectly reasonable thing to do.

You wouldn't normally use a domain service to provide access to data that is inside the aggregate, because the aggregate should already have access to it.

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems($orderService) as $lineItem) {
  ...
}

So that spelling is weird, if we are trying to access this order's collection of line item values. The more natural spelling would be

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems() as $lineItem) {
  ...
}

Of course, this pre-supposes that the line items have been loaded already.

The usual pattern is that the load of the aggregate will include all of the state required for the particular use case. In other words, you may have several different ways of loading the same aggregate; your repository methods are fit for purpose.

This approach isn't something that you will find in the original Evans, where he assumed that an aggregate would have a single data model associated with it. It falls more naturally out of CQRS.

The answer is: definitely NO, avoid passing services in entity methods.

The solution is simple: just let the Order repository return the Order with all of its LineItems. In your case the aggregate is Order + LineItems, so if the repository does not return a complete aggregate, then it's not doing its job.

The broader principle is: keep functional bits (e.g. domain logic) separate from non-functional bits (e.g., persistence).

One more thing: if you can, try to avoid doing this:

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems() as $lineItem) {
  ...
}

Do this instead

$order = $orderService->getOrderByID($orderID);
$order->doSomethingSignificant();

In object-oriented design, we try to avoid fishing around in an object data. We prefer to ask the object to do what we want.

Generally speaking value objects belonging to aggregate don't have repository by themselves. It's aggregate root's responsibility to populate them. In your case, it's your OrderRepository's responsibility to both populate Order entity and OrderLine values objects.

Regarding, the infrastructure implementation of the OrderRepository, in case ORM, it's one-to-many relationship, and you can elect either eagerly or lazy load the OrderLine.

I'm not sure what your services exactly mean. It's quite close to "Application Service". If this is the case, it's generally not a good idea to inject the services to Aggregate root/Entity/Value Object. Application Service should be the client of Aggregate root/Entity/Value Object and Domain Service. Another thing about your services is, exposing value objects in Application Service is not a good idea either. They should be accessed by aggregate root.

Licensed under: CC-BY-SA with attribution
scroll top