Oop 在聚合根DDD的子实体上强制使用作用域不变量
我试图理解如何表示某些DDD(领域驱动设计)规则。 根据蓝皮书公约,我们有:Oop 在聚合根DDD的子实体上强制使用作用域不变量,oop,domain-driven-design,aggregateroot,invariants,Oop,Domain Driven Design,Aggregateroot,Invariants,我试图理解如何表示某些DDD(领域驱动设计)规则。 根据蓝皮书公约,我们有: 根实体具有全局标识,并负责检查不变量 根实体控制访问,不能被其内部的更改所蒙蔽 对内部成员的临时引用只能传递给单个操作使用 当客户可以访问内部实体时,我很难找到执行不变量的最佳方法 当然,只有当子实体是可变的时,这个问题才会发生 请使用这个玩具示例,其中您有一辆汽车和四个轮胎。我想独立跟踪每个轮胎的使用情况 显然,Car是一个聚合根,Tire是一个子实体 业务规则:不能将Milage添加到单个轮胎。当连接到汽车时,
- 根实体具有全局标识,并负责检查不变量
- 根实体控制访问,不能被其内部的更改所蒙蔽
- 对内部成员的临时引用只能传递给单个操作使用
汽车
和四个轮胎
。我想独立跟踪每个轮胎的使用情况
显然,Car
是一个聚合根,Tire
是一个子实体
业务规则:不能将Milage添加到单个轮胎
。当连接到汽车时,Milage只能添加到所有4个轮胎上
一个简单的实现是:
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
方法是公共的,这意味着任何服务都可以执行以下操作:
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);
我想到的可能解决方案:
在轮胎上创建事件
以验证更改,并在汽车上实施更改
使汽车
成为轮胎
的工厂,将轮胎
传递给其制造商,并保留对其的引用
但我觉得应该有一个更简单的方法
你觉得怎么样?轮胎不应该有吸气剂
成功者会给你带来麻烦。除去getter不仅仅是DDD aggregate根的问题,而是OO、Demeter定律等的问题
想一想你为什么需要从汽车上取下轮胎,并将其功能转移到汽车本身中
对内部成员的临时引用只能传递给单个操作使用
自从蓝皮书问世以来,这种做法已经改变;向支持变异操作的内部成员传递引用未完成
考虑这一点的一种方法是采用聚合API(目前支持查询和命令),并将该API拆分为两个(或更多)接口;一个支持命令操作,另一个支持查询
命令操作仍然遵循通常的模式,提供了一条应用程序可以请求聚合自行更改的路径
查询操作返回的接口不包含任何变异操作,既不直接也不通过代理
root.getA() // returns an A API with no mutation operations
root.getA().getB() // returns a B API with no mutation operations
查询是一直向下的查询
在大多数情况下,您可以完全避免查询实体;而是返回表示实体当前状态的值
避免共享子实体的另一个原因是,在大多数情况下,选择将聚合的该部分建模为单独的实体是您可能希望在域模型中更改的决定。通过在API中公开实体,可以在实现选择和API使用者之间创建耦合
(一种想法是:Car聚合不是“Car”,而是一个描述“Car”的“文档”。API应该将应用程序与文档的特定细节隔离开来。)这与Eric Evens的蓝皮书相违背:对内部成员的临时引用只能通过单个操作传递使用。如果聚合是简单的,我可能会这样做,但如果我开始向它添加其他子实体(引擎、门、挡风玻璃等),它会将聚合变成一个外观(非常麻烦).从客户的角度来看,它不再是实体的集合(我想这并不是什么问题,只是说,')那么表达业务行为的方法呢?它们几乎肯定会有副作用。那你会把它放在哪里?在api上?这不是贫血模型吗?@FabioMarreco我想你误解了这个答案。可以把它看作是类Car实现了ImmutableCar、MutableCar
。API是聚合,而不是另一个类。我不知道如何调和无处不在的语言和方法,但这就是我认为的精神。@plalx,对不起。。。误解了答案,(我想我是困了)。这是有道理的。返回值是否表示当前状态解决方案如下所示:不可变的内部状态可以工作,但可能会损害大型状态的性能,我想这是因为对于任何类型的变异,聚合都必须将其全部替换。基于ISP的解决方案不会遇到这样的问题,但正如您所说的,通过类型转换绕过它更容易。然而,在这一点上,如果开发者想把你搞砸,他们总是可以。。。他们可以使用反射来改变状态。这似乎与