当仍然存在无用事件或撤消事件的可能性时,您是否立即将事件应用于域模型?

Do you apply events to the domain model immediately when there is still the possibility of useless events or undo?

我看到的每个事件源示例都是针对网络的。它似乎特别适合 MVC 架构,其中客户端的视图不是 运行 域代码并且 interactivity 是有限的。我不完全确定如何推断出丰富的桌面应用程序,用户可能正在其中编辑列表或执行其他一些较长的 运行 任务。

领域模型是 persistence-agnostic 和 presentation-agnostic,并且只能通过将领域事件应用于聚合根来改变。我的具体问题是 当用户进行未提交的更改时,表示代码是否应该改变域模型?

  1. 如果表示代码不改变域模型,您如何执行域逻辑?当用户编辑时,即时域验证和域计算冒泡到表示模型会很好。否则,您必须在视图模型中复制 non-trivial 逻辑。

  2. 如果表示代码确实改变了域模型,您如何实现撤消?没有域取消删除事件,因为撤消的概念只存在于未提交的编辑 session 中,我不愿意为每个事件添加撤消版本。更糟糕的是,我需要撤消事件的能力 out-of-order。您是否只是删除事件并在每次撤消时重播所有内容? (例如,如果文本字段 returns 恢复到其先前状态,也会发生撤消。)

  3. 如果表示代码确实改变了域模型,是坚持用户执行的每个事件更好还是只将用户的 activity 压缩为尽可能简单的事件集?举个简单的例子,想象一遍又一遍地改变评论字段 在保存之前结束。你真的会坚持每一个中间 CommentChangedEvent 在同一编辑期间同一字段 session?或者对于更复杂的示例,用户将是 更改参数,运行 优化计算,调整 参数,重新运行计算等,直到这个用户 对最近的结果感到满意并提交更改。我 认为没有人会考虑所有中间事件的价值 存储。你如何保持这个浓缩?

存在复杂的协作域逻辑,这让我认为 DDD/ES 是可行的方法。我需要一张富客户端视图模型和域模型如何交互的图片,我希望简单和优雅。

虽然让存储库管理事务,但我最终还是做了这样的事情。

基本上我的仓库都实现了

public interface IEntityRepository<TEntityType, TEventType>{
    TEntityType ApplyEvents(IEnumerable<TEventType> events);
    Task Commit();
    Task Cancel();
}

因此,虽然 ApplyEvents 将更新并 return 实体,但我还在内部保留了起始版本,直到调用 Commit 为止。如果调用取消,我只是将它们交换回来,丢弃事件。

一个非常好的功能是我只在交易完成后将事件推送到网络、数据库等。存储库的实现将依赖于数据库或 Web 服务服务 - 但所有调用代码需要知道的是 Commit 或 Cancel。

编辑 要执行取消操作,您需要将旧实体、更新后的实体和事件存储在内存结构中。像

 public class EntityTransaction<TEntityType, TEventType>
{ 
    public TEntityType oldVersion{get;set;}
    public TEntityType newVersion{get;set;}
    public List<TEventType> events{get;set;}
}

然后你的 ApplyEvents 看起来像,对于用户

private Dictionary<Guid, EntityTransaction<IUser, IUserEvents>> transactions;

public IUser ApplyEvents(IEnumerable<IUserEvent> events)
{
    //get the id somehow
    var id = GetUserID(events);
    if(transactions.ContainsKey(id) == false){
        var user = GetByID(id);
        transactions.Add(id, new EntityTransaction{
             oldVersion = user;
             newVersion = user;
             events = new List<IUserEvent>()
        });
    }

    var transaction = transactions[id];
    foreach(var ev in events){
        transaction.newVersion.When(ev);
        transaction.events.Add(ev);
    }
}

然后在您的取消中,如果您要取消交易,您只需用旧版本替换新版本。

有道理吗?

我认为桌面 DDD 应用程序与 MVC 应用程序没有太大区别,您基本上可以拥有相同的层,只是它们大多不是网络分离的。

CQRS/ES 应用程序与 task-based UI 配合使用效果最佳,您可以在其中发出反映用户意图的命令。但我们所说的任务并不是指用户可以在屏幕上执行的每个操作,它必须在域中具有意义和目的。正如您在 3. 中正确指出的那样,无需将每个微修改建模为完整的 DDD 命令和相关事件。它可能会污染您的事件流。

所以你基本上有两个级别:

UI级动作

这些完全可以在表现层进行管理。它们堆叠起来最终映射到单个命令,但您可以很容易地单独撤消它们。没有什么能阻止您将它们建模为封装 closures for do and undo 的微事件。我从未在任何 UI 中看到 "cherrypickable" 撤消,我也没有真正明白这一点,但这应该是可行的并且用户可以理解,只要操作是可交换的(它们的效果不依赖于执行顺序)。

领域级任务

粗粒度activity 由命令和相应的事件表示。如果你需要撤消这些,我宁愿将一个新的回滚事件附加到事件流中,也不愿尝试删除现有的回滚事件("don't change the past").

反映UI

中的域不变量和计算

这是您真正需要正确区分两种类型任务的地方,因为 UI 操作通常不会更新屏幕上的任何内容,除了一些基本验证(必填字段、字符串和数字格式等)另一方面,发出命令会刷新模型视图,但您可能必须通过某些确认按钮来具体化操作。

如果您的 UI 主要是向用户显示计算的数字和预测,这可能是个问题。您可以将计算放在由 UI 调用的单独服务中,然后在用户保存时使用所​​有更新的计算值发出修改命令。或者,您可以只提交一个带有您更改的参数的命令,并让域调用相同的计算服务。两者实际上更接近 CRUD,并且可能会通过 IMO 导致贫血域模型。