Domain driven design DDD:聚合建模

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

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

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

我的事件存储机制 下面是Greg Young的CQRS文档(请注意PDF“构建事件存储”部分)的广泛启发

我有两个表,一个称为
聚合
,另一个称为
事件
(注意复数形式,因为它们是表,不是对象!),如下所示:

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表格格式,因此,对不起,我将列出):

  • AggregateId
    :基本上是这个表的主键。我使用的是Guid,因为我的所有聚合都使用Guid。
    • AggregateType
      :完全限定聚合的名称
    • CurrentVersion
      :当前聚合版本。每次存储事件时递增的整数
事件表 任何聚合发布的每个域事件都存储在其中;它有5列:

  • AggregateId
    :聚合表的外键
  • SerializedEvent
    :由聚合发出但以序列化形式(如json)的域事件
  • Version
    :每次存储事件时(对于每个给定聚合)递增的整数
  • EventDate
    :日期时间
  • UserName
    :发出事件生成命令的用户
具有2个有界上下文的域示例 现在让我们考虑一个商人:

  • 商人购买产品(这是采购部的工作,又称供应链部)
  • 商人销售产品(这是销售部门的工作,在我们的案例中,假设它是在网站上完成的)
采购部将考虑以下产品:>/P>
  • 该产品可由一个或多个供应商购买
  • 该产品有一个采购定价网格,不同供应商之间可能有所不同
  • 产品存储在一个或多个仓库中,在该仓库中可以(或不可以)获得给定数量的产品
  • 因此,该产品需要库存
另一方面,销售部门将以不同的方式考虑产品:>/P>
  • 产品有一个销售价格(甚至可能有一个销售定价网格)
  • 产品有保证、销售条件
  • 在电子商务环境下,它甚至会有出版物的相关属性(如图片、类别、描述、用户投票和评论…(很可能)
这听起来像两个不同的有界上下文,对吗

事实上,从网站的角度来看,产品的图片、类别和投票属性对我来说就像是第三个有界的上下文,但为了这个例子,我们不要再讨论它了

现在,让我们用域专家规范来完成这个域示例:

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

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

    注:虽然我在一个实际项目中面临着一个非常类似的问题,但这个用例纯粹是抽象的,灵感来自这个Codemotion会议幻灯片

每BC 1个产品聚合=>2个不同的产品AR 好吧,在传统设计中,我可能会得到一个大的
产品实体
,它包含与销售观点和供应观点相关的属性

但我想采用DDD方法,DDD说我应该在有界上下文中保护我的不变量。 因此,产品的领域模型是不同的,这取决于我是在销售范围内还是在供应范围内

据我所知,我应该有两个实体:

  • 销售业务连续性中的产品实体
  • 和供应BC中的另一个产品实体
尽管如此,出于示例的考虑,我们还是承认这两个产品实体决定在各自的BC中提升到聚合根的范围

总而言之,我们有:

2有界上下文

每个有界上下文1个产品聚合

但这是同一种产品,对吗?

在供应链BC中设计
产品AR
以下内容受到广泛启发:

  • @codescribler的博客文章:
  • M.Verraes会议:
首先,让我们看看我的抽象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博客文章的广泛启发:

  • UI(来自供应链dpt的用户)已通过服务层(即将命令转发给其处理程序的命令总线)发送了
    AddProductToCatalogCommand(/*…*/)
  • 处理程序已准备好产品聚合(换句话说,通过将所有以前的事件应用到该聚合,使其处于当前状态),并将命令传递给他

  • 没有提出任何例外(换句话说,Ag
    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....
      }
    
    }