DDD / CQRS / ES - 如何以及在何处实施守卫

DDD / CQRS / ES - How and where to implement guards

早上好,

我有一个模型,其中 User AR 具有特定的 UserRole(管理员、经销商或客户)。对于那个 AR,我会实施一些保护措施,它们是:

假设我想注册一个新的用户。流程如下:

RegisterUser 请求处理程序 -> RegisterUser 命令 -> RegisterUser 命令处理程序 -> User->register(...) 方法 ->UserWasRegistered 域事件

我应该如何以及在何处实施防护以准确验证我的 User 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;
    }
}

如您所见,守卫在 User AR 中,我认为这很糟糕。我想知道我是否应该将这些守卫放在外部验证器或命令处理程序中。另一件事是我可能还应该访问读取模型以确保用户的唯一性和管理器的存在。

最后一件事是,我更愿意为经理传递 UserId VO 而不是 User AR 属性,因此我认为不应将守卫放在 User AR.

非常感谢您的建议。

通常 - 领域驱动设计需要 丰富的 领域模型,这通常意味着业务逻辑位于表示部分领域的方法中。

这通常意味着命令处理程序将负责管道(从数据库加载数据,将更改存储在数据库中),并将计算用户请求结果的工作委托给域模型.

因此 "guards" 通常会在域模型中实现。

And the last thing is, I would prefer pass a User Id rather than a User for the manager property, hence my thinking that guard should not be put in the User model.

没关系 - 当域模型需要非本地信息时,您通常要么查找该信息并将其传递,要么传递查找信息的能力。

所以在这种情况下,您可能会传入一个 "domain service",它知道如何在给定 UserId 的情况下查找 UserRole。

Are you telling me that it is perfectly valid to pass a domain service to an aggregate? At instantiation level or only to the method dealing with?

我强烈建议将服务作为参数传递给需要它们的方法,并且 不是 实例化的一部分。因此,领域模型中的实体持有 数据,并按需提供协作者。

"Domain Service"是Evans在蓝皮书第5章描述的领域模型的第三个元素。在很多情况下,领域服务描述了一个接口(用模型的语言编写),但接口的实现是在应用程序或基础设施中"layer".

所以我永远不会将 存储库 传递给域模型,但我会传递将实际工作委托给存储库的域服务。

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 结束。某些类型的规则(例如访问控制、琐碎的数据类型验证等)在本质上可能不被视为业务规则,因此可以委托给应用层,但核心域规则不应泄漏到域外。

I would prefer pass a UserId value object rather than a User aggregat for the manager property

聚合应该以依赖其边界内的数据来执行规则为目标,因为这是确保强一致性的唯一方法。重要的是要认识到,任何基于聚合外部数据的检查都可能是对陈旧数据进行的,因此仍然可能通过并发违反规则。然后,只有在违规发生后检测违规并采取相应行动,才能使规则最终保持一致。但这并不意味着检查毫无价值,因为它仍然可以防止在低争用情况下发生大多数违规行为。

在向聚合体提供外部信息时,主要有两种策略:

  1. 在调用域之前查找数据(例如在应用程序服务中)

    • 例子(伪代码):

      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, ...);
              ...
          }
      }
      
    • 什么时候使用? 这是最标准的方法,"purest"(聚合从不执行间接 IO)。我总是会首先考虑这个策略。

    • 注意什么? 正如在您自己的代码示例中一样,将 AR 传递到另一个方法中可能很诱人,但我会尽量避免它可以防止传递的 AR 实例发生意外突变,也可以避免对超出需要的合约产生依赖。

  2. 将域服务传递给可用于自行查找数据的域。

    • 例子(伪代码):

      interface RoleLookupService {
          bool userInRole(userId, role);
      }
      
      Application { 
          register(userId, managerId, ...) {
              var registeredUser = User.register(userId, managerId, roleLookupService, ...);
              ...
          }
      }
      
    • 什么时候使用? 当查找逻辑本身足够复杂以至于关心将其封装在域中而不是将其泄漏到域中时,我会考虑这种方法应用层。但是,如果您想维护聚合 "purity",您还可以在应用层所依赖的工厂(域服务)中提取整个创建过程。

    • 注意什么?这里要时刻牢记Interface Segregation Principle,避免传递大合约如IUserRepository当唯一查找的是用户是否具有角色时。此外,这种方法不被认为是 "pure",因为聚合可能正在执行间接 IO。与单元测试的数据依赖性相比,服务依赖性还可能需要更多的模拟工作。

重构原始示例

  • 避免传递另一个 AR 实例
  • 将监管政策明确建模为与特定角色相关联的第一个class公民。请注意,您可以使用规则与角色相关联的任何建模变体。我不一定对示例中的语言感到满意,但您会明白的。

    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 …;
            }
        }
    }