在聚合根的子实体上强制执行具有范围的不变量 - 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);
我想到的可能的解决方案:
- 在
Tire
上创建 events
以验证更改,并在 Car
上实施
- 使
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 应该是将应用程序与文档的具体细节隔离开来。)
我正在尝试了解如何表示某些 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 aCar
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);
我想到的可能的解决方案:
- 在
Tire
上创建events
以验证更改,并在Car
上实施
- 使
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 应该是将应用程序与文档的具体细节隔离开来。)