当我们开始用事件建模我们的系统时,我们很容易掉入陷阱。我们习惯于从数据模型的角度来看待我们的功能:
当你手里拿着一个关系数据库时,你会看到到处都是表格,因为事件设计应该尽可能小,所以事件设计的第一个想法可以是,例如FirstNameChanged。另一个LastNameChanged等。我们也可以立即看到使用,即实体更改的直接历史。这些事件可能如下所示:
public class FirstNameChanged { public string FirstName { get; } public DateTime ChangedAt { get; }
public FirstNameChanged(string firstName, DateTime changedAt) { FirstName = firstName; ChangedAt = changedAt; } }
public class LastNameChanged { public string LastName{ get; } public DateTime ChangedAt { get; }
public LastNameChanged(string lastName, DateTime changedAt) { LastName = lastName; ChangedAt = changedAt; } }
|
然后我们可以直接在 UI 中创建对审计跟踪的更改历史记录。
public class FirstNameChanged { public string PreviousFirstName { get; } public string NewFirstName { get; } public DateTime ChangedAt { get; }
public FirstNameChanged(string previousFirstName, string newFirstName, DateTime changedAt) { PreviousFirstName = previousFirstName; NewFirstName = newFirstName; ChangedAt = changedAt; } }
public class LastNameChanged { public string PreviousLastName { get; } public string NewLastName { get; } public DateTime ChangedAt { get; }
public LastNameChanged(string previousLastName, string newLastName, DateTime changedAt) { PreviousLastName = previousLastName; NewLastName = newLastName; ChangedAt = changedAt; } }
|
即使通过观察这些事件,我们也可以闻到令人不快的气味。很容易看出这种方法是不可维护的。当我们的模型增长时,我们会得到许多微小的、复制/粘贴的、毫无意义的事件。
事件建模的关键方面是让它们接近业务。事件应该直接对应于系统中业务操作的结果。
为了实现这一点,事件应该从特定的请求/命令处理中派生出来。根据命令中发送的值,我们知道传输了哪些数据。基于它们和业务逻辑,我们可以填写事件数据。
名称已更改的事实通常不是影响业务逻辑的因素。(CRUD)
通常,我们只是接受更改,填写数据,仅此而已。因此,我们可以在事件中传递这些信息,并在投影中使用它来构建读取模型。但是,即使我们有 Jiralike 表单来编辑特定字段,也值得根据特征对此类更改进行分组。
我们可以通过更新名字、姓氏等来触发PersonalDataUpdated事件。这些字段可能有Option 类型来检查它们是否已更改。C中此类类型的示例实现可能如下所示:
public struct Option <T> { public static Option <T> None => default; public static Option <T> Some (T value) => new Option <T> (value);
readonly bool isSome; readonly T value;
Option (T value) { this.value = value; isSome = this.value is {}; }
public bool IsSome (out T value) { value = this.value; return isSome; } }
|
那么用法如下:public class PersonalDataUpdated { public Option<string> FirstName { get; } public Option<string> LastName { get; } public DateTime ChangedAt { get; }
public PersonalDataUpdated(string firstName, string lastName, DateTime changedAt) { PreviousLastName = previousLastName; NewLastName = newLastName; ChangedAt = changedAt; } }
var onlyLastNameUpdated = new PersonalDataUpdated( Option<string>.None, Option<string>.Some("Smith") );
|
有了这个,我们没有数百个小事件,而是业务重大事件。
它们仍然包含更改内容的详细信息。我们可以基于它们创建读取模型。假设我们要使用有关先前值和当前值的信息来构建审计跟踪。在这种情况下,我们可以在投影中检索模型的最后状态,将其与事件的变化进行比较,并将差异保存为新行。
发布诸如LastNameChanged 之类的事件称为Property Sourcing。
这是一种反模式。事件本身并没有告诉我们执行它们的操作。它们没有商业价值。由于我们必须生成的事件类型数量众多,管理它们也具有挑战性。其他模块使用它们也不方便。
当然,有时创建字段更改事件是有意义的。例如,EmailUpdated、MartialStateChanged、AccountBalanceUpdated、InvoiceNumberSet。这些是重要的业务领域,可以触发其他工作流程。
良好的事件建模的基础是与业务合作。讨论和理解我们想要达到的目标是基础。当然,有时,停止设计讨论并开始编码是值得的。当我们看到它们在行动时,更容易找到弱点。尽管如此,我们不应该试图通过两周的编码来节省四个小时的讨论时间。
同样重要的是不要将我们的初始事件模型视为一成不变。我们应该接受我们的模型将会改变。我们会更好地了解我们的领域。随着时间的推移,业务也会发生变化。我们应该继续向下钻取,使我们的事件模型更接近现实世界。