当前位置 : 主页 > 手机开发 > 其它 >

域驱动设计 – DDD:建模聚合

来源:互联网 收集:自由互联 发布时间:2021-06-22
我正面临一个设计问题,我想在两个不同的有界环境中对相同的物理对象进行建模. 为了尽可能精确地描述我的问题,甚至我知道这只是一个实现细节,我将从我的事件采购机制开始. 我的事
我正面临一个设计问题,我想在两个不同的有界环境中对相同的物理对象进行建模.

为了尽可能精确地描述我的问题,甚至我知道这只是一个实现细节,我将从我的事件采购机制开始.

我的事件存储机制

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

我有2个表,一个名为Aggregates,另一个名为Events(注意复数形式,因为这些是表,而不是对象!),如下所示:

聚合表

我的所有聚合都存储在此表中;它有3列(SO不支持md表格式,所以,对不起,我会去列表):

> AggregateId:基本上是这个表的主键.我正在使用Guid,因为我的所有聚合都使用了一个.

> AggregateType:完全限定的Aggregate名称.
> CurrentVersion:当前的聚合版本.每次存储事件时递增的整数.

事件表

任何聚合发布的每个域事件都存储在那里;它有5列:

> AggregateId:聚合表的外键.
> SerializedEvent:由聚合发布的域事件,但是以序列化形式(例如,json)
> Version:每次存储事件时递增(对于每个给定聚合)的整数.
> EventDate:日期时间
> UserName:发出事件生成命令的用户

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

现在让我们考虑一个商人:

>商人购买产品(这是采购部门的工作,也就是供应链部门)
>商家销售产品(这是销售部门的工作,在我们的例子中,假设它是在网站上完成的)

采购部门将产品视为以下内容:

>该产品可由一个或多个供应商购买
>该产品具有购买定价网格,可能与供应商不同
>产品存储在一个或多个仓库中,并且在给定数量的可用(或不可用)的地方
>因此产品需要库存

另一方面,销售部门将以不同的方式考虑产品:

>产品有销售价格(甚至可能是销售定价网格)
>产品有保证,销售条件……
>在电子商务环境中,它甚至会有出版物的相关属性(如图片,类别,描述,用户投票和评论……)(很可能)

that sounds like 2 distinct bounded contexts, right?

实际上,从网站的角度看,产品的图片,类别和投票属性,听起来像是第三个有限的背景,但是为了这个例子我们不要讨论它……

现在让我们使用域专家规范完成此域示例:

>“产品必须有名称”
>“供应链部门负责向系统添加产品”
>“因此,销售部门从不向系统添加产品,而是收到NewProductAdded通知,通知他新产品可供销售”
>(可能还有一些其他规则,例如Sales Dptmt只能在供应链Dpt.表示该产品在仓库中可用时才在网站上发布产品.)

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

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

每个BC = 1的产品聚合物2个不同的产品AR

好吧,在传统设计中,我可能最终得到一个大的产品实体,其中包含与销售观点相关的属性以及供应观点.

但是我想采用DDD方法,DDD说我应该在有限的上下文中保护我的不变量.
因此,产品的领域模型根据销售额或供应有限上下文中的不同而不同.

据我了解,我应该有2个实体:

>销售BC中的产品实体
>和BC供应中的另一个产品实体……

仍然为了示例,我们承认这两个产品实体决定被提升到各自BC的聚合根范围.

总结一下,我们有:

2 bounded context

1 product Aggregate per bounded context

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

在供应链BC中设计产品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(/*…*/)
>处理程序已准备好Product Aggregate(换句话说,通过将所有先前的事件应用于它来使其达到当前状态)并将命令传递给他.
>由于没有引发异常(换句话说,Aggregate正确处理了命令),我们现在正处于处理程序请求更改刚才应用于自身的聚合的位置.

处理程序现在将更改保留在数据库中:

>它在Aggregates表中插入一个新行:
> AggregateId = ProductId
> AggregateType = / some / namespace / Product
> AggregateVersion = 0
>它在Events表中插入一个新行:
> AggregateId = ProductId
> Event = ProductWasAddedToCatalog($productId)(以序列化形式关闭课程)
>版本= 0

> Persitence运行良好,因此处理程序将事件转发到服务层(也称为将事件转发给其处理程序的事件总线),以便其订阅者完成其工作.

here comes my problem!

其中一个订阅者是为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));

  }
}

So now, what is my $command->productId?

正如Jimmy Bogard在http://goo.gl/QHBkSr总结的那样:“每个聚合体都有一个根实体[…]根实体具有全局身份,并且最终负责检查不变量”

全球认同是关键词.

所以在我的用例中,我们有2个不同的聚合,因此我们应该有2个区别AggregateRoot的ID.

根据上面描述的事件存储机制,它更加明显,因为如果两个AR具有相同的Id,则在处理其公共静态函数时会有一些事件接收另一个事件reconstituteFrom(AggregateHistory $history)

So 2 distinct Ids. But still it’s the very same product right? How do i make that explicit?

可能的解决方案

经过调查,我想出了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中识别完全相同的物理对象(在该用例中,产品).

Thx in advance for ur help!!!

NB.虽然我的第一篇文章包含许多语言错误,因此遗漏了许多解释的大门,导致对我试图解决的问题的误解,我选择完全重新编辑它.为了让未来的读者理解以前的回复和评论,我留下下面的第一个帖子版本

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

问题是4月18日11:51

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

领域专家是一个商人,他购买,销售和转移产品.他有:

>用于销售目的的电子商务网站
>负责采购目的的供应链部门
>负责运输的物流部门

因此,我们可以考虑为每个有界上下文设置一个Product Aggregate:

>用于销售上下文的产品聚合,用于保存销售价格,evtl等属性.折扣,客户友好的描述,图片,也许它所属的一些类别等.
>购买上下文的产品汇总,其中包含对供应商及其采购条件的引用(例如,按数量,可用性等定价……)
>物流上下文包含物品尺寸和重量等属性(请注意,在此上下文中,聚合物的名称是物品而不是产品,因为后勤部门并不关心它是产品,包装还是包装物品)

领域专家还说:

>“购买部门是将新产品插入系统的部门”
>“一旦采购部门在系统中插入了新产品,就必须为其他人提供”

Now my question is simple :

how do I reference each aggregate to each other, given it’s in the end
“the very same Product”?

Should Sales and Logisitics Aggregates contains an PurchasedProductId
? I’ve been told to be very carefull with external references but …
how else?

编辑:

必须在事件存储模式的基础上看到这个问题,其中:

>每个聚合都以其唯一ID存储在聚合表中(其中行为AggregateId,AggregateType,CurrentVersion),
>此唯一ID在事件表中用作外键(在3列中存储发生在AR上的所有事件:AggregateId,SerializedEvent,Version)

因此,如果应该使用相同的ProductId,因为@Plalx在其回复中建议它,问题变为:

how can you have 2 Aggregates using the same Id whereas, by definition, an Aggregate is a self-containing Entity and, still by definition, an Entity must have a unique 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.

无论如何,我希望这有帮助.

网友评论