域驱动设计 – DDD:建模聚合
我正面临一个设计问题,我想在两个不同的有界环境中对相同的物理对象进行建模.
为了尽可能精确地描述我的问题,甚至我知道这只是一个实现细节,我将从我的事件采购机制开始. 我的事件存储机制 以下内容受到Greg Young的CQRS文档https://cqrs.wordpress.com/documents/的广泛启发(注意PDF“构建事件存储”部分). 我有2个表,一个名为Aggregates,另一个名为Events(注意复数形式,因为这些是表,而不是对象!),如下所示: 聚合表 我的所有聚合都存储在此表中;它有3列(SO不支持md表格式,所以,对不起,我会去列表): > AggregateId:基本上是这个表的主键.我正在使用Guid,因为我的所有聚合都使用了一个. > AggregateType:完全限定的Aggregate名称. 事件表 任何聚合发布的每个域事件都存储在那里;它有5列: > AggregateId:聚合表的外键. 具有2个有界上下文的域的示例 现在让我们考虑一个商人: >商人购买产品(这是采购部门的工作,也就是供应链部门) 采购部门将产品视为以下内容: >该产品可由一个或多个供应商购买 另一方面,销售部门将以不同的方式考虑产品: >产品有销售价格(甚至可能是销售定价网格)
实际上,从网站的角度看,产品的图片,类别和投票属性,听起来像是第三个有限的背景,但是为了这个例子我们不要讨论它…… 现在让我们使用域专家规范完成此域示例: >“产品必须有名称” 现在我认为我们有一个有效的用例. 注意:虽然我在一个真实的项目中遇到了一个非常类似的问题,但这个用例纯粹是抽象的,并且受到了Codemotion会议幻灯片http://goo.gl/lMWSFZ的启发. 每个BC = 1的产品聚合物2个不同的产品AR 好吧,在传统设计中,我可能最终得到一个大的产品实体,其中包含与销售观点相关的属性以及供应观点. 但是我想采用DDD方法,DDD说我应该在有限的上下文中保护我的不变量. 据我了解,我应该有2个实体: >销售BC中的产品实体 仍然为了示例,我们承认这两个产品实体决定被提升到各自BC的聚合根范围. 总结一下,我们有:
但这是完全相同的产品吗? 在供应链BC中设计产品AR 以下内容受到以下广泛启发: > @ codescribler的博文:http://goo.gl/UgYRqq 首先,让我们看看我的抽象AggregateRoot类: 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博客文章的广泛启发:http://goo.gl/yuIjzf > UI(来自供应链dpt.的用户)通过服务层发送了AddProductToCatalogCommand(/*…*/) 处理程序现在将更改保留在数据库中: >它在Aggregates表中插入一个新行: > Persitence运行良好,因此处理程序将事件转发到服务层(也称为将事件转发给其处理程序的事件总线),以便其订阅者完成其工作.
其中一个订阅者是为Sales BC Product Aggregates发出命令的事件处理程序. namespace DomainModel/WriteSide/Sales; use DomainModel/WriteSide/AggregateRoot as BaseAggregate; Class Product extends BaseAggregate { private $productId; //some other attributes related to the Sales BC,like sales price,guarantees... public static function AddAutomaticallyProductToCatalogSinceSupplyChainAddedIt(UpdateSalesCatalogCommand $command) { // some invariants' protection code here $this->recordThat(new ProductWasAutomaticallyAddedToSalesCatalog($command->productId)); } }
正如Jimmy Bogard在http://goo.gl/QHBkSr总结的那样:“每个聚合体都有一个根实体[…]根实体具有全局身份,并且最终负责检查不变量” 全球认同是关键词. 所以在我的用例中,我们有2个不同的聚合,因此我们应该有2个区别AggregateRoot的ID. 根据上面描述的事件存储机制,它更加明显,因为如果两个AR具有相同的Id,则在处理其公共静态函数时会有一些事件接收另一个事件reconstituteFrom(AggregateHistory $history)
可能的解决方案 经过调查,我想出了3种可能的解决方案.我希望有人能引导我进入正确的… 解决方案1:持有参考 销售BC Product Aggregate持有对供应链Product Aggregate的引用. 这看起来像这样: 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... } 解决方案2:在事件存储中使用复合primarey密钥 虽然我目前使用AggregateId列作为主键,但我可以使用AggregateId和AggregateType. 因为那样我可以让两个产品AR都具有相同的ProductId,这对我来说就像是一种气味……单独因为AR全球身份的概念会被破坏…… 解决方案3:在两个AR中使用产品子实体 仍然来自吉米·博加德的http://goo.gl/QHBkSr,“边界内的实体具有本地特征,仅在聚合体内是唯一的.” 所以我可以建模销售BC Product Aggregate如下: 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... } 虽然这样可以保持两个AR具有相同的productId,但这对我来说真的没有意义,因为获得聚合的唯一方法是通过其AR的Id(而不是其任何子实体的Id). 我们可以想象在Query端有一种映射器: 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.... } } 在这个ProductMapper旁边,查询端只会知道ProductId.好像都做了…… 结论 这是一个虚假的用例,因此,上述2个有界上下文是否应该这样建模可能是有争议的. 但我希望我明确指出,即如何在2个不同的BC中识别完全相同的物理对象(在该用例中,产品).
NB.虽然我的第一篇文章包含许多语言错误,因此遗漏了许多解释的大门,导致对我试图解决的问题的误解,我选择完全重新编辑它.为了让未来的读者理解以前的回复和评论,我留下下面的第一个帖子版本 ================================================== ================ 问题是4月18日11:51 让我们直接从上下文开始(取自此Codemotion会议幻灯片http://goo.gl/lMWSFZ). 领域专家是一个商人,他购买,销售和转移产品.他有: >用于销售目的的电子商务网站 因此,我们可以考虑为每个有界上下文设置一个Product Aggregate: >用于销售上下文的产品聚合,用于保存销售价格,evtl等属性.折扣,客户友好的描述,图片,也许它所属的一些类别等. 领域专家还说: >“购买部门是将新产品插入系统的部门”
编辑: 必须在事件存储模式的基础上看到这个问题,其中: >每个聚合都以其唯一ID存储在聚合表中(其中行为AggregateId,AggregateType,CurrentVersion), 因此,如果应该使用相同的ProductId,因为@Plalx在其回复中建议它,问题变为:
解决方法
哇,这里有很多东西.建模AR既是科学也是艺术!
第一点建议:在设计AR时不要涉及您的数据库.为什么? CQRS,AR和事件采购都是DDD的战略和战术模式.重点是消除建模过程中的干扰.数据库是一种分心(在这种情况下).这可能是你遇到困难的根本原因. 除其他外,有界上下文是一种简化建模的机制.它们应反映各部门如何查看产品/项目等内容.事实上,这个模型就是一个很好的例子.模型名称反映了企业在每个上下文中使用的单词.在某些方面,当他们谈论相同的事情时,他们是不同的.它们在各自的背景下意味着不同的东西.因此需要单独对它们进行建模. 外部参考怎么样…… AR可以引用另一个AR但仅以ID的形式(不一定是数据库密钥).实际上,AR不得在其自身内包含对另一个AR的引用,即.包含另一个AR(具有a)的私有变量.这是因为AR只能保证在其边界内保持一致. 这就把我们带到了问题中的问题.我们如何从不同的有界环境中协调这3个AR? 第一种方法是询问它们是否实际上处于不同的有界环境中.有时,这些建模问题是触发重新思考模型的有用方法. 让我们假设您的域名是正确的.我们如何协调他们? 在这种情况下,流程管理器和反腐败层似乎是一个不错的选择.流程管理器将监听产品和/或项目创建的事件.然后它将生成用于创建其他实体的适当命令.机会是,每个上下文都以不同的方式处理.因此需要ACL. ACL将负责将请求转换为在其域内有意义的内容.这可能就像将原始AR的外部ID添加到命令以创建它的AR一样简单.或者它可能只是在暂存区域中保存信息,直到满足各种其他条件. 在较高的层次上,监听事件并使用它们来触发其他有界上下文中的相关过程.使用进程管理器(如果需要)和ACL. 最后存储问题…… 我会在这里选择一个简单的事件存储策略.将每个事件保存在流中.使用AR ID可以撤回任何单个AR的事件. 对于读取模型,我将使用一组监听事件流的非规范化器.然后,他们将生成针对UI定制的读取模型(在这种情况下).这可能涉及组合来自不同BC的信息.无论对用户有什么意义. 我在博客上的帖子中介绍了其中的一些想法:4 Secrets to Inter Aggregate Communication. 无论如何,我希望这有帮助. (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |