DDD, PHP. Domain Object and Business Logic
Asked Answered
A

3

8

I've been very busy with trying to understand the concepts of ddd and Model layer lately. Read tons of articles, examples, Q and A's, spent many hours on it. And still I'm not sure if I got some principles right.

One of them is the answer to the question: How much business logic should exist in Domain Objects? Some sources say Domain Objects should be attached with the whole business logic, on the other hand, I came across articles where I assumed it should be as tiny, as possible and only represent its values. It makes me really confused.

In my understanding, domain objects are classes, that represent entities in the domain.

So, lets for example, go with Invoice entity. Each invoice consist of its items. To compute invoice value, we must sum all items values (it's very simple example, in the real world there would be cases like adding tax, computing paid value, etc)

class Invoice
{
    public $id;
    public $items = [];
    public $status;

    const STATUS_PAID = 'paid';
    const STATUS_NOT_PAID = 'not_paid';

    public function isPaid()
    {
        return $this->status == self::STATUS_PAID;
    }

    public function getInvoiceValue()
    {
        $sum = 0;
        foreach($this->items as $item) {
            $sum += $item->value;
        }
        return $sum;
    }
}

In my understanding, method isPaid() is in the right place. It refers to its own data. But I'm not sure about getInvoiceValue(). We operate here on other domain objects.

Maybe we should use domain objects just to represent data only, but use some decorators to perform more advanced tasks?

Thanks in advance.

Antibes answered 1/8, 2016 at 9:36 Comment(0)
C
4

How much business logic should exist in Domain Objects? [...] I came across articles where I assumed it should be as tiny as possible and only represent its values.

Beware of the Anemic Domain Model that almost exclusively consists of data and lacks behavior. DDD is about creating a behavior-rich domain model. Thus it's fine to add logic into domain classes.

DDD emphasizes good object oriented design, putting methods and data together, thereby promoting highly cohesive systems.

Conceive answered 1/8, 2016 at 21:14 Comment(3)
thank you for your answer. If we attach much business logic to a single domain object, does it not conflict with SRP?Antibes
DDD wants to enable good OO design. Keep following the Single Responsibility Principle and other sensible OO practices.Conceive
@Antibes Just remember that your domain is more than just domain objects. Business logic is not limited to just existing in aggregate roots and entities. They can live in multiple places, including domain services, commands, etc. The domain is a cohesive collection of classes that represent the Ubiquitous Language. There is no rule, for lack of a better word, that says business rule must live in the aggregate/entity and nowhere else. A good part of that will come down to your particular implementation and design decisions.Phenix
M
3

I'm not sure if there's a right answer for these kind of questions, because applying DDD really depends on the particular domain you're applying it to. There are places where your implementation could be perfectly valid, if it satisfies the business needs. In others, like you mentioned with taxes and the like, it wouldn't. So I'd say that you need to keep asking questions about your domain to fully understand what your needs are before translating them into code.

Having said that, if you have a more complex scenario that requires some extra knowledge of the external world to come up with the value of an invoice, one option would be to represent that in your domain explicitly. In your example, that could be an InvoiceProducer, which could have a signature like:

class InvoiceProducer {

    public function __construct(TaxProvider $taxProvider) {
        $this->taxProvider = $taxProvider;
    }

    public function invoiceFor(array $items) {
        new Invoice($items, $this->calculateValue($items));
    }

    private function calculateValue(array $items) {
        $sum = array_reduce($items, function($acc, $item){
            $acc += $item->value;
        }

        return $this->taxProvider->applyTaxTo($sum);
    }
}

Another option would be to use some sort of Strategy pattern, which would leave your implementation very similar to the way it is now, but you'd pass with your call the way you want the taxation to be calculated:

public function getInvoiceValue(TaxProvider $taxProvider)
{
    $sum = 0;
    foreach($this->items as $item) {
        $sum += $item->value;
    }

    return $taxProvider->applyTaxFor($sum);
}

Again, it really depends on how your specific domain works but, as you can see, the implementation shouldn't be that big of a deal. Is more about how it all fits within your domain.

Makebelieve answered 1/8, 2016 at 10:10 Comment(0)
H
3

How much business logic should exist in Domain Objects?

All of it (if possible). The point of DDD is to capture your business logic in your domain - the various tactical patterns can be used here to help (Aggregates, Entities, Value Objects, Domain Services etc...).

Domain objects are classes, that represent entities in the domain.

The classes in your domain can represent more than just Entities. Aggregates, Entities, Value Objects, Domain Services etc can all be represented by classes in your domain.

But I'm not sure about getInvoiceValue(). We operate here on other domain objects.

The example you give of Invoice is a classic example of an Aggregate - Invoice would contain InvoiceItems. getInvoiceValue() is fine here.

In our case, Invoice is an aggregate. Aggregate Root is Invoice itself, but it is also Entity, right? It has its own identity though (invoice number that is unique).

Yes correct

What are InvoiceItems then? Can I fetch them directly from InvoiceItem repository (if I should create such), or do I always have to operate only on Aggregate?

It depends on your use case. It helps to split the write and read models apart (CQRS). If you are talking about the read side (i.e. reporting) then you bypass the domain model and have objects that represent your read model. This might just be a database query. If you are talking about your write side (i.e. commands, domain) then you would normally have a repository per aggregate root. What your aggregate roots are is a modelling question. You want to create them in such a way as they enforce all your business rules - in your example, if an Invoice needs to load InvoiceItems to enforce rules ("no more than 5 items per invoice" for example) then yes, they should be loaded through the aggregate root

Humidistat answered 1/8, 2016 at 11:55 Comment(1)
thank you for your helpful answer. So - let's confirm if my understanding is correct. In our case, Invoice is an aggregate. Aggregate Root is Invoice itself, but it is also Entity, right? It has its own identity though (invoice number that is unique). What are InvoiceItems then? Can I fetch them directly from InvoiceItem repository (if I should create such), or do I always have to operate only on Aggregate?Antibes

© 2022 - 2024 — McMap. All rights reserved.