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?