在聚合根的子实体上强制执行具有范围的不变量 - DDD

Enforcing invariants with scope on child entity of aggregate root - DDD

我正在尝试了解如何表示某些 DDD(领域驱动设计)规则。 根据蓝皮书公约,我们有:

当客户端可以访问内部实体时,我很难找到执行不变量的最佳方法。

这个问题当然只有在子实体可变时才会发生。

假设这个玩具示例中有一个 Car 和四个 Tire(s)。我想独立地跟踪每个 Tire 的使用情况。

显然 Car 是一个 聚合根 Tire 是一个 子实体 .

Business Rule: Milage cannot be added to to a single Tire. Milage can only be added to all 4 tires, when attached to a Car

A naive 实现将是:

public class Tire
{
    public double Milage { get; private set;  }
    public DateTime PurchaseDate { get; set; }
    public string ID { get; set; }
    public void AddMilage(double milage) => Milage += milage;
}

public class Car
{
    public Tire FrontLefTire { get; private set; }
    public Tire FrontRightTire { get; private set; }
    public Tire RearLeftTire { get; private set; }
    public Tire RearRightTire { get; private set; }

    public void AddMilage (double milage)
    {
        FrontLefTire.AddMilage(milage);
        FrontRightTire.AddMilage(milage);
        RearLeftTire.AddMilage(milage);
        RearRightTire.AddMilage(milage);
    }

    public void RotateTires()
    {
        var oldFrontLefTire = FrontLefTire;
        var oldFrontRightTire = FrontRightTire;
        var oldRearLeftTire = RearLeftTire;
        var oldRearRightTire = RearRightTire;

        RearRightTire = oldFrontLefTire;
        FrontRightTire = oldRearRightTire;
        RearLeftTire = oldFrontRightTire;
        FrontLefTire = oldRearLeftTire;
    }

    //...
}

但是 Tire.AddMilage 方法是 public,这意味着任何服务 都可以 做这样的事情:

Car car = new Car(); //...

// Adds Milage to all tires, respecting invariants - OK
car.AddMilage(200); 

//corrupt access to front tire, change milage of single tire on car
//violating business rules - ERROR
car.FrontLefTire.AddMilage(200); 

我想到的可能的解决方案:

  1. Tire 上创建 events 以验证更改,并在 Car
  2. 上实施
  3. 使 Car 成为 Tire 的工厂,在其构造函数上传递 TireState,并持有对它的引用。

但我觉得应该有更简单的方法来做到这一点。

你怎么看?

轮胎不应该有吸气剂。

吸毒者会给你带来麻烦。去掉 getters 不仅仅是 DDD Aggregte Roots 的问题,而是 OO,Demeter 法则等的问题

想一想为什么你需要汽车的轮胎并将该功能转移到汽车本身。

Transient references to internal members can be passed out for use withing a single operation only.

自蓝皮书编写以来的这些年里,这种做法发生了变化;传递对支持变异操作的内部成员的引用未完成。

一种思考方式是采用聚合 API(目前支持查询和命令),并将 API 拆分为两个(或更多)接口;一个支持命令操作,一个支持查询。

命令操作仍然遵循通常的模式,提供应用程序可以要求聚合更改自身的路径。

查询操作 return 接口包括 没有 变异操作,既不直接也不通过代理。

root.getA() // returns an A API with no mutation operations
root.getA().getB() // returns a B API with no mutation operations

查询是一直向下的查询。

在大多数情况下,您可以完全避免查询实体;而是代表实体当前状态的 return

避免共享子实体的另一个原因是,在大多数情况下,选择将聚合的那部分建模为单独的实体是您可能希望在域模型中更改的决定。通过在 API 中公开实体,您将在该实现选择与 API.

的消费者之间创建耦合

(一种思考方式:Car 聚合不是 "car",它是描述 "car" 的 "document"。API 应该是将应用程序与文档的具体细节隔离开来。)