早上好,
我有一个模型,其中用户 AR 具有特定的用户角色(管理员、经销商或客户)。对于该 AR,我将实现一些防护措施,它们是:
- 管理员不能拥有除他本人以外的管理员
- 经销商不能拥有除管理员以外的经理
- 客户不能拥有除转销商或客户以外的经理(子帐户情况)
假设我想注册一个新的用户。流程如下:
RegisterUser 请求处理程序 -> RegisterUser 命令 -> RegisterUser 命令处理程序 -> User->register(...) 方法 ->UserWasRegistered 域事件
我应该如何以及在哪里实现防护措施来准确验证我的用户 AR?现在,我的东西如下所示:
namespace vendor\Domain\Model;
class User
{
public static function register(
UserId $userId,
User $manager,
UserName $name,
UserPassword $password,
UserEmail $email,
UserRole $role
): User
{
switch($role) {
case UserRole::ADMINISTRATOR():
if(!$userId->equals($manager->userId)) {
throw new \InvalidArgumentException('An administrator cannot have a manager other than himself');
}
break;
case UserRole::RESELLER():
if(!$manager->role->equals(UserRole::ADMINISTRATOR())) {
throw new \InvalidArgumentException('A reseller cannot have a manager other than an administrator');
}
break;
case UserRole::CLIENT():
// TODO: This is a bit more complicated as the outer client should have a reseller has manager
if(!$manager->role->equals(UserRole::RESELLER()) && !$manager->role->equals(UserRole::Client())) {
throw new \InvalidArgumentException('A client cannot have a manager other than a reseller or client');
}
}
$newUser = new static();
$newUser->recordThat(UserWasRegistered::withData($userId, $manager, $name, $password, $email, $role, UserStatus::REGISTERED()));
return $newUser;
}
}
正如你在这里看到的,守卫位于用户 AR 中,我认为这很糟糕。我想知道是否应该将这些 guard 放在外部验证器中或命令处理程序中。另一件事是,我可能还应该访问读取模型,以确保用户的唯一性和管理者的存在。
最后一件事是,我更愿意为经理属性传递一个UserId VO,而不是一个User AR,因此我认为不应该放置 guard 用户 AR。
非常感谢您的建议。
最佳答案
As you can see here, guards are in the model himself which I think is bad. I'm wondering if I should either put those guards in external validators or in the command handler.
通过 DDD,您努力将业务逻辑保留在域层中,更具体地说,尽可能保留在模型(聚合、实体和值对象)中,以避免最终出现 Anemic Domain Model 。某些类型的规则(例如访问控制、简单数据类型验证等)本质上可能不被视为业务规则,因此可以委托(delegate)给应用程序层,但核心域规则不应泄漏到域外。
I would prefer pass a UserId value object rather than a User aggregat for the manager property
聚合的目标应该是依靠其边界内的数据来执行规则,因为这是确保强一致性的唯一方法。重要的是要认识到,任何基于聚合外部数据的检查都可能是对过时的数据进行的,因此并发性仍然可能违反规则。只有在违规行为发生后进行检测并采取相应行动,才能使规则最终保持一致。但这并不意味着检查毫无值(value),因为它仍然可以防止大多数违规行为在低争用情况下发生。
在向聚合提供外部信息时,有两种主要策略:
在调用域之前查找数据(例如在应用程序服务中)
示例(伪代码):
Application { register(userId, managerId, ...) { managerUser = userRepository.userOfId(userId); //Manager is a value object manager = new Manager(managerUser.id(), managerUser.role()); registeredUser = User.register(userId, manager, ...); ... } }
何时使用?这是最标准的方法,也是“最纯粹的”(聚合从不执行间接 IO)。我总是首先考虑这个策略。
要注意什么? 就像在您自己的代码示例中一样,将 AR 传递到另一个方法中可能很诱人,但我会尽力避免它以防止意外的突变传递 AR 实例,并避免创建对超出需要的合约的依赖。
将域服务传递到域,域可以使用该服务自行查找数据。
示例(伪代码):
interface RoleLookupService { bool userInRole(userId, role); } Application { register(userId, managerId, ...) { var registeredUser = User.register(userId, managerId, roleLookupService, ...); ... } }
什么时候使用?当查找逻辑本身足够复杂,需要关心将其封装在域中而不是将其泄漏到应用程序层时,我会考虑这种方法。但是,如果您想保持聚合的“纯度”,您还可以在应用程序层所依赖的工厂(域服务)中提取整个创建过程。
要注意什么?您应该始终保留 Interface Segregation Principle请记住,当唯一要查找的内容是用户是否具有角色时,请避免传递诸如 IUserRepository 之类的大型合约。此外,这种方法不被认为是“纯粹的”,因为聚合可能正在执行间接 IO。与单元测试的数据依赖项相比,服务依赖项可能还需要更多的工作来模拟。
重构原始示例
- 避免传递另一个 AR 实例
将监管政策明确建模为与特定角色相关的一等公民。请注意,您可以使用规则与角色关联的任何建模变体。我不一定对示例中的语言感到满意,但您会明白的。
interface SupervisionPolicy { bool isSatisfiedBy(Manager manager); } enum Role { private SupervisionPolicy supervisionPolicy; public SupervisionPolicy supervisionPolicy() { return supervisionPolicy; } ... } class User { public User(UserId userId, Manager manager, Role role, ...) { //Could also have role.supervisionPolicy().assertSatisfiedBy(manager, 'message') which throws if not satsified if (!role.supervisionPolicy().isSatisfiedBy(manager)) { throw …; } } }
关于php - DDD/CQRS/ES - 如何以及在何处实现防护,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54476846/