Domain driven design DDD:聚合建模
我面临一个设计问题,我想在两个不同的有界上下文中对同一个物理对象建模 为了尽可能精确地描述我的问题,即使我知道这只是一个实现细节,我将从我的事件源机制开始 我的事件存储机制 下面是Greg Young的CQRS文档(请注意PDF“构建事件存储”部分)的广泛启发 我有两个表,一个称为Domain driven design DDD:聚合建模,domain-driven-design,cqrs,aggregateroot,Domain Driven Design,Cqrs,Aggregateroot,我面临一个设计问题,我想在两个不同的有界上下文中对同一个物理对象建模 为了尽可能精确地描述我的问题,即使我知道这只是一个实现细节,我将从我的事件源机制开始 我的事件存储机制 下面是Greg Young的CQRS文档(请注意PDF“构建事件存储”部分)的广泛启发 我有两个表,一个称为聚合,另一个称为事件(注意复数形式,因为它们是表,不是对象!),如下所示: namespace DomainModel/WriteSide/Sales; use DomainModel/WriteSide/Aggreg
聚合
,另一个称为事件
(注意复数形式,因为它们是表,不是对象!),如下所示:
namespace DomainModel/WriteSide/Sales;
use DomainModel/WriteSide/AggregateRoot as BaseAggregate;
Class Product extends BaseAggregate
{
private $productId;
private $supplyChainProductId; //the reference to the supply chain BC Product AR...
public function getAggregateId()
{
return $this->productId;
}
//more methods there...
}
汇总表
我所有的聚合都存储在这个表中;它有3列(因此不支持md表格格式,因此,对不起,我将列出):
:基本上是这个表的主键。我使用的是Guid,因为我的所有聚合都使用Guid。AggregateId
:完全限定聚合的名称AggregateType
:当前聚合版本。每次存储事件时递增的整数CurrentVersion
:聚合表的外键AggregateId
:由聚合发出但以序列化形式(如json)的域事件SerializedEvent
:每次存储事件时(对于每个给定聚合)递增的整数Version
:日期时间EventDate
:发出事件生成命令的用户UserName
- 商人购买产品(这是采购部的工作,又称供应链部)
- 商人销售产品(这是销售部门的工作,在我们的案例中,假设它是在网站上完成的)
- 该产品可由一个或多个供应商购买
- 该产品有一个采购定价网格,不同供应商之间可能有所不同
- 产品存储在一个或多个仓库中,在该仓库中可以(或不可以)获得给定数量的产品
- 因此,该产品需要库存
- 产品有一个销售价格(甚至可能有一个销售定价网格)
- 产品有保证、销售条件
- 在电子商务环境下,它甚至会有出版物的相关属性(如图片、类别、描述、用户投票和评论…(很可能)
- “产品必须有名称”
- “供应链部门负责向系统添加产品”
- 因此,销售部门从不向系统中添加产品,而是收到一个NewProductAdded通知,通知他新产品可供销售
- (可能还有一些其他规则,如销售部Dptmt,只有在供应链部Dpt表示该产品在仓库中可用时,才能在网站上发布该产品。) 现在我认为我们有了一个有效的用例 注:虽然我在一个实际项目中面临着一个非常类似的问题,但这个用例纯粹是抽象的,灵感来自这个Codemotion会议幻灯片
产品实体
,它包含与销售观点和供应观点相关的属性
但我想采用DDD方法,DDD说我应该在有界上下文中保护我的不变量。
因此,产品的领域模型是不同的,这取决于我是在销售范围内还是在供应范围内
据我所知,我应该有两个实体:
- 销售业务连续性中的产品实体
- 和供应BC中的另一个产品实体
产品AR
以下内容受到广泛启发:
- @codescribler的博客文章:
- M.Verraes会议:
namespace DomainModel/WriteSide;
abstract class AggregateRoot
{
protected $lastRecordedEvents = [];
protected function recordThat(DomainEvent $event)
{
$this->latestRecordedEvents[]=$event;
$this->apply($event);
}
protected function apply(DomainEvent $event)
{
$method = 'apply'.get_class($event);
$this->$method($event);
}
public function getUncommittedEvents()
{
return $this->lastestRecordedEvents;
}
public function markEventsAsCommitted()
{
$this->lastestRecordedEvents = [];
}
public static function reconstituteFrom(AggregateHistory $history)
{
foreach($history as $event) {
$this->apply($event);
}
return $this;
abstract public function getAggregateId();
}
基本上,这个类拥有ES机制
现在让我们看一下它在供应链BC中的产品实现:
namespace DomainModel/WriteSide/SupplyChain;
use DomainModel/WriteSide/AggregateRoot as BaseAggregate;
Class Product extends BaseAggregate
{
private $productId;
private $productName;
//some other attributes related to the supply chain BC...
public function getAggregateId()
{
return $this->productId;
}
private function __construct(ProductId $productId, $productName)
{
//private constructor allowing factory methods
}
public static function AddToCatalog(AddProductToCatalogCommand $command)
{
//some invariants protection stuff
$this->recordThat(new ProductWasAddedToCatalog($command->productId));
}
private function applyProductWasAddedToCatalog(DomainEvent $event)
{
$newProduct = new Product($event->productId);
return $newProduct;
}
//more methods there...
}
流动
以下是@codescribler博客文章的广泛启发:
AddProductToCatalogCommand(/*…*/)
namespace DomainModel/WriteSide/Sales;
use DomainModel/WriteSide/AggregateRoot as BaseAggregate;
Class Product extends BaseAggregate
{
private $productId;
private $supplyChainProductId; //the reference to the supply chain BC Product AR...
public function getAggregateId()
{
return $this->productId;
}
//more methods there...
}
namespace DomainModel/WriteSide/Sales;
use DomainModel/WriteSide/AggregateRoot as BaseAggregate;
// **here i'd introduce my sub-entity**
use DomainModel/Sales/Product/Entities/Product as ProductEntity;
Class Product extends BaseAggregate
{
private $_Id;
private $product; //holds a ProductEntity instance
public function getAggregateId()
{
return $this->_Id;
}
public function getProductId()
{
return $this->product->getProductId();
}
//more methods there...
}
namespace DomainModel/QuerySide;
Class ProductMapping
{
private $productId;
private $salesAggregateId;
private $supplyChainAggregateId;
private $product; //holds a ProductEntity instance
public function getSalesAggregateId()
{
return $this->salesAggregateId;
}
public function getSupplyChainAggregateId()
{
return $this->supplyChainAggregateId();
}
}
Class ProductMappingRepository
{
public function findByproductId($productId)
{
//return the ProductMapping object
}
public function addFromEvent(DomainEvent $event)
{
//this repository is an event subscriber....
}
}