使用 event-sourcing 时在哪里验证业务规则
Where to validate business rules when using event-sourcing
我实现了事件源实体(在域驱动设计中它被称为聚合)。创建丰富的域模型是一种很好的做法。领域驱动设计 (DDD) 建议尽可能将所有与业务相关的事物放入核心实体和价值 objects.
但是将这种方法与事件溯源结合使用时会出现问题。与事件源系统中的传统方法相比,首先存储事件,然后在构建实体以执行某些方法时应用所有事件。
基于此,最大的问题是将业务逻辑放在哪里。通常,我想要一个像这样的方法:
public void addNewAppointment(...)
在这种情况下,我希望该方法能够确保不违反任何业务规则。如果是这种情况,将抛出异常。
但是在使用事件溯源时,我必须创建一个事件:
Event event = new AppointmentAddedEvent(...);
event store.save(event);
现在,我探索了两种在存储事件之前检查业务规则的方法。
首先检查应用层的业务规则。 DDD中的应用层是委托层。实际上,它不应该包含任何业务逻辑。它应该只委托诸如获取核心实体、调用方法和保存内容之类的事情。在此示例中,将违反此规则:
List<Event> events = store.getEventsForConference(id);
// all events are applied to create the conference entity
Conference conf = factory.build(events);
if(conf.getState() == CANCELED) {
throw new ConferenceClosed()
}
Event event = new AppointmentAddedEvent(...);
event store.save(event);
显然,向取消的会议添加约会的业务规则不应该泄漏到 non-core 组件中。
我知道的第二种方法是在核心实体中添加命令的处理方法:
class Conference {
// ...
public List<Event> process(AddAppointmentCommand command) {
if(this.state == CANCELED) {
throw new ConferenceClosed()
}
return Array.asList(new AppointmentAddedEvent(...));
}
// ...
}
在这种情况下,好处是业务规则是核心实体的一部分。但是这里违反了关注点分离原则。现在,该实体负责创建存储在事件存储中的事件。除此之外,一个实体负责创建事件感觉很奇怪。我可以争论为什么实体可以处理事件是很自然的。但是为了存储而不是为了自然发布而创建领域事件感觉不对。
你们中有人遇到过类似的问题吗?你是如何解决这些问题的?
现在,我将只讨论应用程序服务解决方案中的业务规则。它仍然是一个地方 ok-ish 但它违反了一些 DDD 原则。
我期待着您关于 DDD、事件溯源和传入更改验证的想法和经验。
提前致谢
我喜欢这个问题。当我第一次问这个问题时,那是在遵循模式和挑战自己去理解到底发生了什么之间的转折点。
the big question is where to put the business logic
通常的答案是 "the same place you did before"——在域实体的方法中。你的"second approach"是通常的想法。
But there is a violation of separation of concerns principle.
不是真的,但看起来确实很奇怪。
考虑一下我们通常在保存当前状态时所做的事情。我们 运行 进行一些查询(通常通过存储库)以从记录簿中获取原始状态。我们使用该状态来创建实体。然后我们 运行 命令,实体在其中创建新状态。然后我们将对象保存在存储库中,它用记录簿中的新状态替换原始状态。
在代码中,它看起来像
state = store.get(id)
conf = ConferenceFactory.build(state)
conf.state.appointments.add(...)
store.save(id, conf.state)
我们在事件溯源中真正做的是用持久的事件集合替换可变状态
history = store.get(id)
conf = ConferenceFactory.build(history)
conf.history.add(AppointmentScheduled(...))
store.save(id, conf.history)
在成熟的业务领域,如会计或银行业,无处不在的语言包括事件历史:journal
、ledger
、transaction history
、...诸如此类。在那些情况下,事件历史是域的固有部分。
在其他领域——比如日历安排——我们(还没有?)在领域语言中没有类似的实体,所以当我们更改为事件时,感觉就像我们在做一些奇怪的事情。但核心模式是相同的——我们从记录簿中提取历史记录,我们操纵该历史记录,我们将更新保存到记录簿中。
所以业务逻辑发生在它总是发生的同一个地方。
也就是说,是的,领域逻辑知道事件。
一个可能有帮助的练习:放开 "object oriented" 约束,只考虑函数....
static final List<Event> scheduleAppointment(List<Event> history, AddAppointmentCommand addAppointment) {
var state = state(history)
if(state == CANCELED) {
throw new ConferenceClosed()
}
return Array.asList(new AppointmentAddedEvent(...));
}
private static final State state(List<Event> history) {...}
我实现了事件源实体(在域驱动设计中它被称为聚合)。创建丰富的域模型是一种很好的做法。领域驱动设计 (DDD) 建议尽可能将所有与业务相关的事物放入核心实体和价值 objects.
但是将这种方法与事件溯源结合使用时会出现问题。与事件源系统中的传统方法相比,首先存储事件,然后在构建实体以执行某些方法时应用所有事件。
基于此,最大的问题是将业务逻辑放在哪里。通常,我想要一个像这样的方法:
public void addNewAppointment(...)
在这种情况下,我希望该方法能够确保不违反任何业务规则。如果是这种情况,将抛出异常。
但是在使用事件溯源时,我必须创建一个事件:
Event event = new AppointmentAddedEvent(...);
event store.save(event);
现在,我探索了两种在存储事件之前检查业务规则的方法。
首先检查应用层的业务规则。 DDD中的应用层是委托层。实际上,它不应该包含任何业务逻辑。它应该只委托诸如获取核心实体、调用方法和保存内容之类的事情。在此示例中,将违反此规则:
List<Event> events = store.getEventsForConference(id);
// all events are applied to create the conference entity
Conference conf = factory.build(events);
if(conf.getState() == CANCELED) {
throw new ConferenceClosed()
}
Event event = new AppointmentAddedEvent(...);
event store.save(event);
显然,向取消的会议添加约会的业务规则不应该泄漏到 non-core 组件中。
我知道的第二种方法是在核心实体中添加命令的处理方法:
class Conference {
// ...
public List<Event> process(AddAppointmentCommand command) {
if(this.state == CANCELED) {
throw new ConferenceClosed()
}
return Array.asList(new AppointmentAddedEvent(...));
}
// ...
}
在这种情况下,好处是业务规则是核心实体的一部分。但是这里违反了关注点分离原则。现在,该实体负责创建存储在事件存储中的事件。除此之外,一个实体负责创建事件感觉很奇怪。我可以争论为什么实体可以处理事件是很自然的。但是为了存储而不是为了自然发布而创建领域事件感觉不对。
你们中有人遇到过类似的问题吗?你是如何解决这些问题的?
现在,我将只讨论应用程序服务解决方案中的业务规则。它仍然是一个地方 ok-ish 但它违反了一些 DDD 原则。
我期待着您关于 DDD、事件溯源和传入更改验证的想法和经验。
提前致谢
我喜欢这个问题。当我第一次问这个问题时,那是在遵循模式和挑战自己去理解到底发生了什么之间的转折点。
the big question is where to put the business logic
通常的答案是 "the same place you did before"——在域实体的方法中。你的"second approach"是通常的想法。
But there is a violation of separation of concerns principle.
不是真的,但看起来确实很奇怪。
考虑一下我们通常在保存当前状态时所做的事情。我们 运行 进行一些查询(通常通过存储库)以从记录簿中获取原始状态。我们使用该状态来创建实体。然后我们 运行 命令,实体在其中创建新状态。然后我们将对象保存在存储库中,它用记录簿中的新状态替换原始状态。
在代码中,它看起来像
state = store.get(id)
conf = ConferenceFactory.build(state)
conf.state.appointments.add(...)
store.save(id, conf.state)
我们在事件溯源中真正做的是用持久的事件集合替换可变状态
history = store.get(id)
conf = ConferenceFactory.build(history)
conf.history.add(AppointmentScheduled(...))
store.save(id, conf.history)
在成熟的业务领域,如会计或银行业,无处不在的语言包括事件历史:journal
、ledger
、transaction history
、...诸如此类。在那些情况下,事件历史是域的固有部分。
在其他领域——比如日历安排——我们(还没有?)在领域语言中没有类似的实体,所以当我们更改为事件时,感觉就像我们在做一些奇怪的事情。但核心模式是相同的——我们从记录簿中提取历史记录,我们操纵该历史记录,我们将更新保存到记录簿中。
所以业务逻辑发生在它总是发生的同一个地方。
也就是说,是的,领域逻辑知道事件。
一个可能有帮助的练习:放开 "object oriented" 约束,只考虑函数....
static final List<Event> scheduleAppointment(List<Event> history, AddAppointmentCommand addAppointment) {
var state = state(history)
if(state == CANCELED) {
throw new ConferenceClosed()
}
return Array.asList(new AppointmentAddedEvent(...));
}
private static final State state(List<Event> history) {...}