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?

有帮助吗?

解决方案

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.

其他提示

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.

许可以下: CC-BY-SA归因
scroll top