在并发编辑环境中为子实体强制实施不变量

Mat*_*att 1 c# domain-driven-design

鉴于子集合不能超过x个项目的不变量,域名如何保证在并发/ Web环境中强制实施这种不变量?让我们看一个(经典)示例:

我们有一个ManagerEmployees.(假设的)不变量表明经理不能有超过七个直接报告Employee.我们可以这样(天真地)实现这样:

public class Manager {

    // Let us assume that the employee list is mapped (somehow) from a persistence layer
    public IList<Employee> employees { get; private set; }

    public Manager(...) {
        ...
    }

    public void AddEmployee(Employee employee) {

        if (employees.Count() < 7) {
            employees.Add(employee);
        } else {
            throw new OverworkedManagerException();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

直到最近,我才认为这种方法足够好.然而,似乎还有就是它使数据库来存储边缘的情况下超过七名员工,从而打破不变.考虑这一系列事件:

  1. 人员A在UI中编辑管理员
    (内存中有6名员工,数据库中有6名员工)
  2. 人员B在UI中编辑管理员
    (内存中有6名员工,数据库中有6名员工)
  3. B人员添加员工并保存更改
    (内存中有7名员工,数据库中有7名员工)
  4. 人员A添加员工并保存更改
    (内存中有7名员工,数据库中有8名员工)

当再次从数据库中提取域对象时,Manager构造函数可能(或可能不)强化Employee集合上的计数不变量,但无论哪种方式,我们现在都有数据与我们的不变量所期望的差异.我们如何防止这种情况发生?我们如何干净利落地从中恢复?

Voi*_*son 5

考虑这一系列事件:

Person A goes to edit Manager in UI
(6 employees in memory, 6 employees in database)
Person B goes to edit Manager in UI
(6 employees in memory, 6 employees in database)
Person B adds Employee and saves changes
(7 employees in memory, 7 employees in database)
Person A adds Employee and saves changes
(7 employees in memory, 8 employees in database)
Run Code Online (Sandbox Code Playgroud)

最简单的方法是将数据库写入实现为比较和交换操作.所有写入都使用聚合的陈旧副本(毕竟,我们正在查看内存中的聚合,但记录簿是磁盘上的持久副本).关键的想法是,当我们实际执行写入时,我们还检查我们正在使用的陈旧副本仍然是记录簿中的实时副本.

(例如,在事件源系统中,您不会附加到流中,而是附加到流中的特定位置 - 即,您希望尾指针位于何处.因此在竞赛中,只有一个写入获得提交到尾部位置;另一个在并发冲突上失败并重新开始.)

在Web环境中对此类似可能是使用eTag,并在执行写入时验证etag是否仍然有效.获胜者获得成功回复,失败者获得412前提条件失败.

对此的改进是为您的域使用更好的模型. Udi Dahan写道:

时间上的微秒差异不应对核心业务行为产生影响

具体来说,如果您的模型因为命令A和B碰巧以不同的顺序处理而最终处于不同的状态,那么您的模型可能与您的业务不匹配.

您的示例中的模拟将是两个命令都应该成功,但两者中的第二个也应该设置一个标志,指出聚合当前不合规.当addEmployee命令和removeEmployee命令碰巧在传输层中以错误的方式进行排序时,此方法可防止出现这种情况.

(假设的)不变量表明经理不能有超过七个直接报告

需要警惕的是 - 即使在假设的例子中,数据库是否是记录簿.数据库很少在现实世界中获得否决权.如果现实世界是记录簿,你可能不应该拒绝改变.