【问题标题】:Mixing together DDD with Event Sourcing将 DDD 与事件溯源混合在一起
【发布时间】:2018-01-04 08:42:46
【问题描述】:

我无法理解将 DDD 与 ES 混合在一起的概念。我认为事件是域方面的一部分。鉴于将它们从存储库发布到外部世界并保持模型纯粹和简单没有问题。但除此之外,还必须有可能在特定聚合上重放它们。这就是我的问题发生的地方。我想让我的域模型保持与 lib/framework 无关的纯粹和简单的对象。

要在聚合上应用过去的事件,聚合必须知道它是 ES 结构的一部分(因此它不会保持纯域对象)。由于聚合的主要工作是保护一些可能随时间演变的业务不变量,因此使用聚合 API 应用旧事件是不可能的。例如,有带有子实体 Comments 的聚合 Post。 Today Post 允许添加 10 条评论,并且方法 addCommnet() 保护该规则。但它并不总是那样。一年前,用户最多可以添加 20 条评论。所以应用过去的事件可能不符合当前规则。

Broadway(流行的 PHP CQRS 库)通过在没有任何预验证的情况下应用事件来解决该问题。方法 addCommnet() 只是根据我们的不变量检查它,然后处理应用事件。 Applyinig 事件本身不做任何进一步的检查。这很好,但我认为这是我的领域模型中的高水平集成。我的领域模型真的需要了解有关基础设施(这是 ES 保存数据的风格)的任何信息吗?

编辑: 用最简单的话来说明问题:有没有机会从聚合中摆脱所有那些 applyXXX() 方法?

编辑2: 我已经用 PHP 编写了这个想法的(有点老套)PoC - github

【问题讨论】:

    标签: php domain-driven-design event-sourcing


    【解决方案1】:

    免责声明:我是CQRS framework 人。

    Broadway(流行的 PHP CQRS 库)通过在没有任何预验证的情况下应用事件来解决该问题。

    这就是每个 CQRS 聚合的工作方式,不会检查事件,因为它们表达了过去已经发生的事实。这意味着应用 event 永远不会抛出 exceptions

    要将过去的事件应用于聚合,聚合必须知道它是 ES 结构的一部分(因此它不会保持纯域对象)

    不,它没有。它必须知道它过去的事件。这很好。

    Today Post 允许添加 10 条评论,并且方法 addCommnet() 保护该规则。但它并不总是那样。一年前,用户最多可以添加 20 条评论。所以应用过去的事件可能不符合当前规则。

    是什么让您aggregate 不会忽略该事件或与 1 年前不同的解释?! 这种特殊情况应该让您思考 CQRS 的强大功能:写入与读取具有不同的逻辑。您将事件应用于聚合以验证到达它的未来命令(写入/命令端)。显示这 20 个事件由其他逻辑(读取/查询端)处理。

    这是我的问题发生的地方。我想保持我的领域模型纯粹而简单的对象,这些对象与 lib/framework 无关。

    CQRS 可以使您的聚合保持纯净(无副作用),不依赖任何库且简单。我使用cqrs.nu 提供的样式,通过产生事件来做到这一点。这意味着聚合命令处理程序方法实际上是generators

    读取模型也可以非常简单,普通的 PHP 不可变对象。只有读取模型更新程序依赖于持久性,但可以使用 interface 来反转。

    【讨论】:

    • 请注意那些强制聚合扩展某些类、接口、使用某些静态类方法或全局对象的框架。
    • 这正是我的问题,干扰我的领域模型的框架。我可以改变商业规则,没关系。我正在寻找从聚合之外应用过去(和今天不正确)事件的方法。
    • @ayeo 在我的框架中,每个聚合每个事件都公开一个 applyXXX 方法,每个命令公开一个 handleXXX 方法;命令处理程序 yields 事件是 applied 到聚合本身。
    • 我理解这种方法,但我需要将其分开。我想拥有专用的命令处理程序类(这很容易)和专用的“事件应用程序”。这对我很重要,因为我不想将我的域模型(例如聚合)与实现细节(cqrs/es)联系起来。 Afaik 域不应该依赖于基础设施。因此,如果我决定以其他方式保存我的数据,我的业务模型应该保持不变。我不确定这是否可能:)
    • @ayeo 这被称为一种风格。有很多款式。我的风格与命令和事件完美契合。经典的 OOP 风格并不完全适合命令和事件,它需要一个翻译层。在我看来,混合这两种风格会带来意外的复杂性。
    【解决方案2】:

    我无法理解将 DDD 与 CQRS 混合在一起的概念。

    从表面上看,您无法完全理解 DDD 和 事件溯源 的组合。 CQRS 和事件溯源是不同的想法(恰好可以很好地结合在一起)。

    Today Post 允许添加 10 条评论,并且方法 addCommnet() 保护该规则。但它并不总是那样。一年前,用户最多可以添加 20 条评论。所以应用过去的事件可能不符合当前规则。

    这绝对是真的。但是请注意,如果您有一个包含 15 个 cmets 的 non 事件来源的帖子,并且您现在尝试制定一个“规则”,现在只允许 10 个 cmets,那么您仍然有一个问题。

    我对这个难题(两种风格)的回答是,您需要对所涉及的职责有稍微不同的理解。

    领域模型的职责是行为;它描述了可以从当前状态到达的状态。领域模型不应该限制你处于坏状态,它应该防止好的状态变成坏状态。

    在版本一中,我们可以说Post 的状态包括TwentyList of Comments,其中 TwentyList 是(令人惊讶的)一个最多可以容纳 20 个评论标识符的容器。

    在第二版中,我们希望保持 10 cmets 的限制,我们不会将 TwentyList 更改为 TenList,因为这给我们带来了向后兼容性问题。相反,我们将域规则更改为“不得将任何 cmets 添加到具有 10 个或更多 cmets 的帖子中”。数据schema不变,不良状态仍然可以表示,但允许的状态transitions受到很大限制。

    具有讽刺意味的是,Greg Young 的Versioning in an Event Sourced System 是一本值得阅读以获取更多见解的好书。从高层次上讲,事件版本控制只是消息版本控制,状态只是先前模型为当前模型留下的消息。

    值类型与规则约束无关,它们与语义约束有关。

    请记住,时间线非常不同;行为是关于现在下一个,但状态是关于过去。状态应该比行为更持久(这意味着对设计资本的相应投资)。

    我的领域模型真的需要了解有关基础设施的任何信息(这是 ES 保存数据的方式)吗?

    不,域模型不需要了解基础架构。

    但事件不是基础设施——它们是状态AddCommentRemoveComment 事件的日志是状态,就像 Comment 条目的列表是状态一样。

    “行为”的最一般形式是将当前状态作为输入并发出事件作为输出的函数

    List<Event> act(State currentState);
    

    因为我们总是可以在外层,获取事件(这是状态的非破坏性表示,并从中构建状态。

    State act(State currentState) {
        List<Event> changes = act(currentState)
        State nextState = currentState.apply(changes)
        return nextState
    }
    
    List<Event> act(List<Event> history) {
        State initialState = new State();
        State currentState = initialState.apply(changes)
        return act(currentState)
    }
    
    State act(List<Event> history) {
        // Writing this out long hand to drive home the point
        // we could of course call act: List<Event> -> State
        // to avoid duplication.
    
        List<Event> changes = act(history)
        State initialState = new State()
        State currentState = initialState.apply(history)
    
        State nextState = currentState.apply(changes)
    
        return nextState;
    }
    

    关键是您可以在最一般的情况下实现行为,添加一些适配器,然后让管道选择最合适的实现。

    再次强调,责任分离是您的指路明灯:管理现状的状态、管理允许更改的行为以及管道/基础设施都是不同的关注点。

    用最简单的话来说:我正在寻找机会从我的聚合中删除许多 applyXXX() (或类似的具有重载方法的语言)方法

    applyXXX 只是一个函数,它接受StateEvent 作为参数并返回一个新的State。您可以使用任何您想要的拼写和范围。

    【讨论】:

    • 我可以接受随着时间的推移改变商业规则,这很好。我正在寻找从聚合之外应用过去(并且今天不正确)事件的方法。我认为事件是我领域的一部分,聚合发出/注册/发布它们。我不想在我的聚合中有 apply() 方法,因为我认为它是 ES 的一部分(而我的域对此并不关心)。最后,我想知道是否可以不直接将我的聚合与重放事件相关联。但是,由于业务凝聚力验证,我无法从总体上做到这一点。
    • 用最简单的话来说:我正在寻找机会从我的聚合中摆脱许多 applyXXX() (或类似的具有重载方法的语言)方法 :)
    【解决方案3】:

    我的回答很简短。确实,您要处理的是事件溯源,而不是 CQRS。

    如果某些事件的处理随着时间的推移而发生变化,那么您实际上有两种情况:

    1. 您正在修复一个错误,并且您的处理程序的行为应该有所不同。在这种情况下,您只需继续进行更改。
    2. 你有了一些新的意图。你实际上有一个新的处理方式。这意味着实际上这是一个不同的事件。在这种情况下,您有一个新事件和新处理程序。

    这些场景与编程语言和框架无关。一般来说,事件溯源更多的是关于业务而不是任何技术。

    我会赞成 Greg 的书推荐。

    【讨论】:

    • 你完全正确的是ES关注。我读过书。我的问题是将 applyXXX() 方法移到聚合之外。当方法被放置在里面时,我可以应用过去的事件,而无需根据当前的要求检查它们(例如,最大帖子数,如果有人在过去添加了 20 并且今天最多添加了 10 - 那很好)。通过专用类回复事件的问题(据我所知)是它必须使用聚合 api(它将根据当前逻辑进行验证)。我的问题不在于改变商业规则。它更多地在于技术层面。 – ayeo 14 分钟前
    【解决方案4】:

    我认为您的问题是您希望在应用事件时对其进行验证,但应用和验证是聚合操作的两个不同阶段。当您通过方法 addComment(event) 添加评论时,您需要验证逻辑并且此方法正在引发事件,当您回复事件时,此逻辑不会再次检查。过去的事件无法更改,并且如果您的聚合通过回复事件引发异常,则您的聚合有问题。我就是这么理解你的问题的。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-03-07
      • 1970-01-01
      • 2013-01-24
      • 1970-01-01
      • 2016-02-20
      • 1970-01-01
      • 2020-03-20
      • 1970-01-01
      相关资源
      最近更新 更多