gpt4 book ai didi

domain-driven-design - DDD:建模聚合

转载 作者:行者123 更新时间:2023-12-01 09:55:09 26 4
gpt4 key购买 nike

我面临一个设计问题,我想在两个不同的有界上下文中对同一物理对象进行建模。

为了尽可能准确地描述我的问题,甚至我意识到这只是一个实现细节,我将从事件源机制开始。

我的事件存储机制

以下内容受到Greg Young的CQRS文档https://cqrs.wordpress.com/documents/的广泛启发(请注意PDF“构建事件存储”部分)。

我有 2个表,一个表称为Aggregates,另一个表称为Events(注意,复数形式是表,而不是对象!)看起来像这样:

汇总表

我所有的汇总都存储在此表中;它具有3列(因此不支持md表格式,因此,抱歉,我会列出):

  • AggregateId:基本上是该表的主键。我正在使用Guid,因为我的所有聚合都使用了Guid。
  • AggregateType:完全限定的聚合名称。
  • CurrentVersion:当前的聚合版本。每次存储事件时都会增加的整数。

  • 活动表

    任何聚合发出的每个域事件都存储在其中;它有5列:
  • AggregateId:聚集表的外键。
  • SerializedEvent:聚集发布但以序列化形式(例如json)发布的域事件
  • Version:每次存储事件时(对于每个给定的聚合)递增的整数。
  • EventDate:日期时间
  • UserName:发出事件的生产命令
  • 的用户

    具有2个有界上下文的域的示例

    现在考虑一个商人:
  • 商家购买产品(这是采购部门(也称为供应链部门)的工作)
  • 商家销售产品(这是销售部门的工作,在我们的情况下,它说是在网站上完成的)

  • 采购部门将考虑以下产品:
  • 该产品可由一个或多个供应商购买
  • 产品具有购买定价网格,可能与供应商不同
  • 产品以给定数量存储在一个(或多个)可用(或不可用)的仓库中。
  • 产品因此要受库存

  • 另一方面,销售部门将以不同的方式考虑产品:
  • 产品具有销售价格(甚至还有销售价格网格)
  • 产品有保证,销售条件...
  • 在电子商务环境中,
  • 甚至会具有出版物的相关属性(例如图片,类别,描述,用户投票和评论...)(很可能是)

  • 听起来像2个不同的有界上下文,对吗?

    实际上,从网站的角度来看,产品及其图片,类别和投票属性对我来说听起来像是第三个有界上下文,但出于示例的原因,我们不要就此争论不休...

    现在,让我们用域专家规范完成这个域示例:
  • “产品必须具有名称”
  • “供应链部门是负责向系统添加产品的部门”
  • “因此,销售部门从不向系统添加产品,而是收到NewProductAdded通知,通知他有新产品可用于销售。”
  • (也许其他一些规则,例如Sales Dptmt只能在供应链Dpt表示该产品在仓库中可用后才能在网站上发布产品。)

    现在我认为我们有一个有效的用例。

    注意:虽然我在一个实际项目中面临着一个非常类似的问题,但是这个用例纯粹是抽象的,并且受到了Codemotion会议幻灯片http://goo.gl/lMWSFZ的启发。

  • 每个BC 1个产品汇总=> 2个不同的产品AR

    好的,在传统设计中,我可能最终会得到一个很大的 Product Entity,其中包含的属性与Sales观点和Supply观点一样相关。

    但是我想采用DDD方法,DDD说我应该在有限的上下文中保护我的不变量。
    因此,产品的领域模型会根据我在“销售”范围内还是在“供应范围内”上下文而有所不同。

    据我了解,因此我应该有2个实体:
  • 销售BC中的产品实体
  • 和供应BC中的另一个产品实体...

  • 仍然出于示例的目的,我们承认这两个产品实体已决定被提升为各自BC中的集合根范围。

    综上所述,我们有:

    2有界上下文

    每个有界上下文汇总1个产品

    但是这是完全相同的产品吗?

    在BC供应链中设计 Product AR
    以下内容受到以下方面的广泛启发:
  • @codescribler的博客文章:http://goo.gl/UgYRqq
  • M. Verraes会议:http://goo.gl/iVrdZu

  • 首先,让我们看一下我的抽象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(/*...*/)
  • 处理程序已准备好产品集合(换句话说,通过对它应用所有先前的事件使它达到当前状态)并将命令传递给他。
  • 假定没有引发异常(换句话说,Aggregate正确处理了该命令),所以现在是处理程序在请求更改刚刚应用于自身的聚合的地方。

    处理程序现在将更改保留在数据库中:
  • 它将在Aggregates表中插入新行:
  • AggregateId = ProductId
  • AggregateType = / some / namespace / Product
  • AggregateVersion = 0
  • 它将在Events表中插入新行:
  • AggregateId = ProductId
  • 事件= ProductWasAddedToCatalog($productId)(当然是序列化形式)
  • 版本= 0
  • 持久性进行得很好,因此处理程序将事件转发到服务层(又称为事件总线,将事件转发到其处理程序 s ),以供其订阅者完成其工作。

  • 这是我的问题!

    这些订阅者之一是事件处理程序,它为Sales BC产品集合发出命令。
    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));

    }
    }

    那么,现在我的$ command-> productId是什么?

    正如吉米·博加德(Jimmy Bogard)在 http://goo.gl/QHBkSr中总结的那样:“每个聚合都有一个根实体[...]根实体具有 全局标识,并最终负责检查不变式”

    全局身份是关键字。

    因此,在我的用例 中,我们有2个不同的聚合,因此我们应该有2个不同的AggregateRoot的Id

    根据上述事件存储机制,这一点更加明显,因为如果两个AR具有相同的ID,则一个AR在处理其 public static function reconstituteFrom(AggregateHistory $history)时会收到另一个ID的事件

    所以有2个不同的ID。但这仍然是同一产品吗?我该如何明确?

    可能的解决方案

    经过调查,我提出了3种可能的解决方案。我希望有人能够引导我进入正确的...

    解决方案1:持有参考

    销售BC产品汇总包含对供应链产品汇总的引用。

    这看起来像这样:
    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:在事件存储中使用复合primaryy密钥

    虽然我目前使用 AggregateId列作为主键,但是我可以同时使用 AggregateIdAggregateType

    因为那将使我两个产品AR都具有相同的ProductId,所以对我来说这似乎是一种气味...由于AR全局标识的概念而独自一人被破坏了...

    解决方案3:在两个AR中使用产品子实体

    仍来自吉米·鲍嘉(Jimmy Bogard)的 http://goo.gl/QHBkSr,“边界内的实体具有本地身份,仅在集合体内是唯一的”。

    因此,我可以对销售BC产品汇总建模,如下所示:
    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)。

    我们可以想象在查询端有一种映射器:
    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个有界上下文进行建模,这值得商bat。

    但是我希望我能阐明我的观点,即如何在2个不同的BC中识别相同的物理对象(在该用例中为产品)。

    提前 Thx为您提供帮助!!!

    注意虽然我的第一篇文章包含许多语言上的误用,因此为解释留下了许多打开的门,导致对我要解决的问题的误解,但我选择完全重新编辑它。为了让以后的读者理解以前的答复和评论,我在下面保留了第一个帖子版本

    ================================================== ================

    根据4月18日11:51提出的问题

    让我们从上下文开始(摘自此Codemotion会议幻灯片 http://goo.gl/lMWSFZ)。

    领域专家是商人,他购买,销售和转移产品。他有:
  • 用于销售目的的电子商务网站
  • 负责采购目的的供应链部门
  • 负责运输
  • 的物流部门

    因此,我们可以考虑为每个有界上下文使用一个产品集合:
  • 用于销售上下文的产品集合,将包含销售价格,evtl等属性。折扣,客户友好的说明,图片,也许它也属于某些类别,等等。
  • 采购上下文的产品集合,将包含对供应商及其采购条件的引用(例如,按数量,库存状况等定价)。
  • 用于物流上下文的项目汇总,具有诸如大小和重量之类的属性(请注意,在此上下文中,汇总的名称是Item而不是Product,因为物流部门并不担心它是产品,包装还是内部商品)

  • 领域专家还说:
  • “采购部门是将新产品插入系统的部门”
  • “一旦采购部门在系统中插入了新产品,则其他产品必须可用”

    现在我的问题很简单:

    鉴于最终,我如何互相引用每个聚合
    “同一个产品”?

    销售和物流汇总应包含PurchasedProductId
    ?有人告诉我要谨慎对待外部参考,但是...
    能怎样?

    编辑:

    必须根据事件存储模式来查看此问题,其中:
  • 每个聚合及其唯一ID都存储在聚合表中(其中的行是AggregateId,AggregateType,CurrentVersion),
  • 此唯一ID在事件表中用作外键(该表在3列中存储了发生在AR上的所有事件:AggregateId,SerializedEvent,Version)

  • 因此,如果在@Plalx的答复中建议使用相同的ProductId,问题将变为:

    您如何才能有2个使用相同ID的聚合,而根据定义,聚合是一个自包含实体,并且仍然按照定义,一个实体必须具有唯一的ID?

    最佳答案

    哇,这里有很多事。对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中介绍了其中一些想法。

    无论如何,我希望这会有所帮助。

    关于domain-driven-design - DDD:建模聚合,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/29716944/

    26 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com