1

I have some doubts about having rich domain model without having other entity objects inside it.

Let's imagine that we have a product feed with no more than 10 products.

Each product is made by some vendor from some country.

For some reasons we want to hide all products from some country.

With having other objects inside our aggregate this can be done with something like this:

final class ProductFeed implements AggregateRoot
{
    private FeedId $id;
    /**
     * @var Product[]
     */
    private array $products = [];

    private array $domainEvents = [];

    public function __construct(FeedId $id)
    {
        $this->id = $id;
    }

    public function id(): FeedId
    {
        return $this->id;
    }

    public function addProduct(Product $product): void
    {
        Assert::lessThan(count($this->products), 10);
        $product->putToFeed($this);
        $this->products[] = $product;
    }

    public function hideAllFromCountry(Country $country): void
    {
        foreach ($this->products as $product) {
            if ($product->vendor()->country()->equals($country)) {
                $product->hide();
                $this->record(new ProductWasHidden($product->id()));
            }
        }
    }

    public function pullDomainEvents(): array
    {
        $events = $this->domainEvents;
        $this->domainEvents = [];
        return $events;
    }

    private function record(DomainEventInterface $domainEvent): void
    {
        $this->domainEvents[] = $domainEvent;
    }
}

final class Product
{
    private ProductId $id;
    private ProductFeed $feed;
    private ProductVendor $productVendor;
    private bool $visible = true;

    public function __construct(ProductId $id, ProductVendor $productVendor)
    {
        $this->id = $id;
        $this->productVendor = $productVendor;
    }

    public function id(): ProductId
    {
        return $this->id;
    }

    public function putToFeed(ProductFeed $feed): void
    {
        $this->feed = $feed;
    }

    public function vendor(): ProductVendor
    {
        return $this->productVendor;
    }

    public function hide(): void
    {
        $this->visible = false;
    }

    public function show(): void
    {
        $this->visible = true;
    }
}

final class ProductVendor
{
    private VendorId $id;
    private Country $country;

    public function __construct(VendorId $id, Country $country)
    {
        $this->id = $id;
        $this->country = $country;
    }

    public function country(): Country
    {
        return $this->country;
    }
}

class Country
{
    private string $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function equals(Country $country): bool
    {
        return $this->name === $country->name;
    }
}

// Client code
$firstVendor = new ProductVendor(
    VendorId::next(),
    new Country('Canada')
);
$secondVendor = new ProductVendor(
    VendorId::next(),
    new Country('USA')
);

$firstProduct = new Product(
    ProductId::next(),
    $firstVendor
);

$secondProduct = new Product(
    ProductId::next(),
    $secondVendor
);

$feed = new ProductFeed(FeedId::next());
$feed->addProduct($firstProduct);
$feed->addProduct($secondProduct);

$feed->hideAllFromCountry(new Country('Canada'));
$feed->pullDomainEvents(); // get here domain event with first product id

So far so good. However, after reading document about designing effective domain aggregates by Vaugh Vernon, I am not sure that by referencing other entities only by unique ID, I can keep rich domain model, because code would look something like this:

final class ProductFeed implements AggregateRoot
{
    private FeedId $id;
    /**
     * @var Product[]
     */
    private array $products = [];

    private array $domainEvents = [];

    public function __construct(FeedId $id)
    {
        $this->id = $id;
    }

    public function addProduct(Product $product): void
    {
        Assert::lessThan(count($this->products), 10);
        $product->putToFeed($this);
        $this->products[] = $product;
    }

    /***
     * @param Product[] $products
     */
    public function hide(array $products): void
    {
        foreach ($products as $product) {
            if (in_array($product, $this->products)) {
                $product->hide();
                $this->record(new ProductWasHidden($product->id()));
            }
        }
    }

    public function pullDomainEvents(): array
    {
        $events = $this->domainEvents;
        $this->domainEvents = [];
        return $events;
    }

    private function record(DomainEventInterface $domainEvent): void
    {
        $this->domainEvents[] = $domainEvent;
    }
}

final class Product
{
    private ProductId $id;
    private FeedId $feedId;
    private VendorId $productVendorId;
    private bool $visible = true;

    public function __construct(ProductId $id, VendorId $productVendorId)
    {
        $this->id = $id;
        $this->productVendorId = $productVendorId;
    }

    public function id(): ProductId
    {
        return $this->id;
    }

    public function putToFeed(FeedId $feedId): void
    {
        $this->feedId = $feedId;
    }

    public function vendorId(): VendorId
    {
        return $this->productVendorId;
    }

    public function hide(): void
    {
        $this->visible = false;
    }

    public function show(): void
    {
        $this->visible = true;
    }
}

final class ProductVendor
{
    private VendorId $id;
    private Country $country;

    public function __construct(VendorId $id, Country $country)
    {
        $this->id = $id;
        $this->country = $country;
    }

    public function country(): Country
    {
        return $this->country;
    }
}

class Country
{
    private string $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function equals(Country $country): bool
    {
        return $this->name === $country->name;
    }
}

interface ProductRepositoryInterface {
    /**
     * @return Product[]
     */
    public function findByCountryAndFeed(Country $country, FeedId $feedId): array;
}

$firstVendor = new ProductVendor(
    VendorId::next(),
    new Country('Canada')
);
$secondVendor = new ProductVendor(
    VendorId::next(),
    new Country('USA')
);

$firstProduct = new Product(
    ProductId::next(),
    $firstVendor
);

$secondProduct = new Product(
    ProductId::next(),
    $secondVendor
);

$feed = new ProductFeed(FeedId::next());
$feed->addProduct($firstProduct);
$feed->addProduct($secondProduct);

$repo = new ProductRepository();
$products = $repo->findByCountryAndFeed(new Country('Canada'), $feed->id());

$feed->hide($products);
$feed->pullDomainEvents(); // get here domain event first first product id

So, the question is, how to have rich domain model with referencing other entities only by their unique ids?

1
  • 2
    I don't entirely subscribe to Vaugh Vernons approach to DDD, but it is a way to go about it. The idea isn't to reference all other entities by ID, but to reference other aggregates by ID ( aggregates being small clusters of objects that work together, and are manipulated externally via their root object's interface). This then clearly delineates aggregate boundaries. So aggregates themselves can be behavior-rich, and you can design them around important pieces of your domain logic. But this approach does push some of the behavior to repositories, services, or other outer layer objects. – Filip Milovanović Sep 12 '20 at 15:19
0

Having a "rich" domain model, i.e. having objects with behavior is antithetical to Vaughn Vernon's approach of having data structures and services. The former is basically what object-orientation is, the latter is roughly procedural programming.

This is not a value judgement. I'm just saying you can't mix these things. You either go for objects, or you take advice from Vaughn Vernon. You can't do both.

1
  • Ok, thank you, got it, thought that there could be some kind of solution – Andrii Filenko Sep 14 '20 at 5:55
1

Preliminary remark: For consistency with your terminology, I'll use "reference" here in the sense of a unique ID that allows to refer to an object, and not in the traditional sense of a binary object handle or pointer to access an object in memory.

What is rich?

What makes according to you a rich domain model ?

  • Is it a lot of large objects that are entirely in memory? Should the richness of the model measured according to the RAM one has to afford to use it?
  • Is it a lot of objects with a lot of connections ? Or should the richness be measured according to the information that can be combined when it is needed?

Or otherwise stated, is your browser rich in information, because you can load a big HTML document without reference? Or is it rich in information, becaus it allows you to browse a wealth of information on wikipedia, with lots of small sized articles that are interlinked using named references and "URL"?

The fact of having references or not does not express anything on richness of information and power of expressivity of the model: with a simple graph model, with nodes and edges between referred nodes, I can save the world (yes! with all the cities and the roads)! Try to do the same without references.

What do you need?

There is no best solution. THe question is what do you need:

  • The first approach may be faster navigate. But it's slower to load (i.e. user might have to wait), and comes with practical limits (performance, size,...). But with very few products in you feed, it doesn't really matter.
  • The second may be faster to work with: you can start to use it before everything related is loaded (lazy loading). Morover, it has no practical limits: you could work with an almost infinite feed if you design your processing using a stream architecture. But navigation might be slower if you access to referred objects not yet loaded.

I don't like dogmas. In the end, it's always a balance to find between the constraints to best meet your own requirements.

Final remark

The use of references is a little overhead that can easily be mastered, for example using the repository and an identity map. This approach is not theory but has proven itself in a large number of applications.

Using a reference approach allows you to abstract your feed: instead of a limited list of products, you could plug the feed on a large catalogue, and define active filters. In this case you could hide some countries, show only some categories of products, or even dynamically chose what to sho or not based on personalized preferences. For this reason, I'd personally opt for a reference approach.

4
  • Thank you for your answer Christophe, but my main concern is not an implementation, but the thing that the logic of filtering would leak from Feed to some service and Feed would become just an event factory – Andrii Filenko Sep 13 '20 at 8:30
  • @AndriiFilenko I see. Do I get it right: So your problem is not so much the references by themselves, but the publicity that they'd get via the events; a kind of security/confidentialiy concern? – Christophe Sep 13 '20 at 9:24
  • no, the problem is having domain logic inside domain entities and not in services (like with anemic model) – Andrii Filenko Sep 14 '20 at 5:54
  • @AndriiFilenko Could there be a misunderstanding here? DDD doesn’t promote an anemic model: it puts domain logic in the entities and aggregates and services according to the principle of separation of concerns (i.e. not puting in an entity what is not related to the entity and could change/evolve independently of it) – Christophe Sep 14 '20 at 6:19

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.