如何使聚合根更小
How to make aggregate root smaller
怎么回事
我正在开发使用一次性密码处理操作确认的内部微服务。我决定使用事件溯源方法创建良好的领域模型。
我有什么
我有确认会话 用于操作 某个操作类型。用户可以添加询问 Session 以获得 Confirmation Attempt 并指定他想要接收 Channel 挑战(一次性密码)。 频道 就像短信、电子邮件、推送等
这是命令处理程序的代码摘录
var session = _sessionFactory.CreateSession(operationType, userId);
var challengeCreationResult = await _challengeProvider.CreateAsync(
recipient, channelType, session, addOnFields);
if (!challengeCreationResult.IsSuccess)
{
return OperationResult<AttemptCreationResult>.Error(
challengeCreationResult.ErrorCode, challengeCreationResult.ErrorMessage);
}
session.AddAttempt(challengeCreationResult.Data);
创建确认尝试并收到挑战后,用户能够提交他的验证响应。
var session = await _sessionStore.GetAsync(sessionId);
var result = await session.ApplyResponseAsync(response, _responseValidator, actingUserId, operationType);
AddAttempt
和 ApplyResponseAsync
都会触发适当的事件,return 操作结果 return 到 用户 。
出了什么问题
在尝试添加需要了解多个 Confirmation Session 的业务规则之前,我对这一实施感到满意。它们是:
- 如果用户在一小时内针对单个操作类型给出了五个错误答案,我们需要冻结他一小时。
- 只有在自上次没有正确答案的确认尝试 30 秒后,才允许用户请求新的确认尝试。
问题
如何在保持聚合 root/services/event 处理程序小而集中的同时实施这些规则?
我认为 UserConfirmation
作为聚合看起来不错,但不是 UserConfirmationHistory
的想法,因为不建议有很多子级的聚合,否则会出现这种情况这里。
在这种情况下,业务规则自然不适合聚合,应向上移动到域服务,该域服务可以在应用程序服务提供的 UserConfirmation
个实例列表上运行。
我的意思是,我根本不喜欢在我的域 classes 中有任何基础设施问题(无论是聚合还是服务),并且获取 UserConfirmation
条目的列表需要查询或存储库实现,这是基础架构问题,即使您使用接口抽象它们也是如此。
我会做什么
- 应用程序服务:持有对
IUserConfirmationRepository
的引用并获取 UserConfirmation
条目的列表,将它们传递给...
- 域服务:它将接收
UserConfirmation
个条目的列表并验证跨越多个聚合的逻辑
这样,您的业务规则仍然受到域 class 的保护。
但谨慎行事:
- 这样做,你可能刚刚打开了一个缺口。如果您的代码的用户能够在不通过此域服务的情况下创建和保留
UserConfirmation
实体的实例,那么他们将能够绕过此不变量;
- 那你就得想办法防止了。也许通过在
UserConfirmation
聚合中创建构造函数 internal
,并使创建此聚合的唯一可能路径是通过域服务。
PS。你提到当你只需要做简单的插入时事情会更容易,但是:
- DDD 并非适用于每个项目的方法。在使用 DDD 的项目早期存在很多摩擦,只有当您的领域非常复杂时,这才是合理的。然后你会在路上抢走好处;
- 如果 DDD 确实是该项目的正确方法,那么以后您将真正受益于这种方式
怎么回事
我正在开发使用一次性密码处理操作确认的内部微服务。我决定使用事件溯源方法创建良好的领域模型。
我有什么
我有确认会话 用于操作 某个操作类型。用户可以添加询问 Session 以获得 Confirmation Attempt 并指定他想要接收 Channel 挑战(一次性密码)。 频道 就像短信、电子邮件、推送等
这是命令处理程序的代码摘录
var session = _sessionFactory.CreateSession(operationType, userId);
var challengeCreationResult = await _challengeProvider.CreateAsync(
recipient, channelType, session, addOnFields);
if (!challengeCreationResult.IsSuccess)
{
return OperationResult<AttemptCreationResult>.Error(
challengeCreationResult.ErrorCode, challengeCreationResult.ErrorMessage);
}
session.AddAttempt(challengeCreationResult.Data);
创建确认尝试并收到挑战后,用户能够提交他的验证响应。
var session = await _sessionStore.GetAsync(sessionId);
var result = await session.ApplyResponseAsync(response, _responseValidator, actingUserId, operationType);
AddAttempt
和 ApplyResponseAsync
都会触发适当的事件,return 操作结果 return 到 用户 。
出了什么问题
在尝试添加需要了解多个 Confirmation Session 的业务规则之前,我对这一实施感到满意。它们是:
- 如果用户在一小时内针对单个操作类型给出了五个错误答案,我们需要冻结他一小时。
- 只有在自上次没有正确答案的确认尝试 30 秒后,才允许用户请求新的确认尝试。
问题
如何在保持聚合 root/services/event 处理程序小而集中的同时实施这些规则?
我认为 UserConfirmation
作为聚合看起来不错,但不是 UserConfirmationHistory
的想法,因为不建议有很多子级的聚合,否则会出现这种情况这里。
在这种情况下,业务规则自然不适合聚合,应向上移动到域服务,该域服务可以在应用程序服务提供的 UserConfirmation
个实例列表上运行。
我的意思是,我根本不喜欢在我的域 classes 中有任何基础设施问题(无论是聚合还是服务),并且获取 UserConfirmation
条目的列表需要查询或存储库实现,这是基础架构问题,即使您使用接口抽象它们也是如此。
我会做什么
- 应用程序服务:持有对
IUserConfirmationRepository
的引用并获取UserConfirmation
条目的列表,将它们传递给... - 域服务:它将接收
UserConfirmation
个条目的列表并验证跨越多个聚合的逻辑
这样,您的业务规则仍然受到域 class 的保护。
但谨慎行事:
- 这样做,你可能刚刚打开了一个缺口。如果您的代码的用户能够在不通过此域服务的情况下创建和保留
UserConfirmation
实体的实例,那么他们将能够绕过此不变量; - 那你就得想办法防止了。也许通过在
UserConfirmation
聚合中创建构造函数internal
,并使创建此聚合的唯一可能路径是通过域服务。
PS。你提到当你只需要做简单的插入时事情会更容易,但是:
- DDD 并非适用于每个项目的方法。在使用 DDD 的项目早期存在很多摩擦,只有当您的领域非常复杂时,这才是合理的。然后你会在路上抢走好处;
- 如果 DDD 确实是该项目的正确方法,那么以后您将真正受益于这种方式