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

时间:2019-02-01 09:47:19

标签: php domain-driven-design cqrs

早上好

我有一个模型,其中 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中,我认为这很糟糕。我想知道是否应该将这些防护措施放在外部验证程序或命令处理程序中。另一件事是,我可能还应该访问读取模型,以确保用户的唯一性和管理者的存在。

最后一件事是,我更愿意为Manager属性传递 UserId VO而不是 User AR,因此我认为不应设置警卫用户 AR。

您的建议将不胜感激。

2 个答案:

答案 0 :(得分:1)

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

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

因此,通常在域模型内实施“保护措施”。

  

最后一件事是,我希望为Manager属性传递用户ID而不是用户,因此我认为不应在用户模型中放置防护。

很好-当域模型需要的信息不是本地的时,通常您可以查找该信息并将其传递,或者传递查找信息的功能。

因此,在这种情况下,您可能要传递一个“域服务”,该服务知道如何在给定UserId的情况下查找UserRole。

  

您是否告诉我,将域服务传递给聚合是完全有效的吗?在实例化级别还是仅对方法进行处理?

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

“域服务”是蓝本第5章中Evans描述的域模型的第三个元素。在许多情况下,域服务描述了一个接口(以模型的语言编写),但是该接口的实现是在应用程序或基础结构的“层”中进行的。

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

答案 1 :(得分:1)

  

正如您在此处看到的那样,警卫在模型本身中,我认为这很糟糕。我想知道是否应该将这些防护措施放在外部验证程序或命令处理程序中。

使用DDD,您将努力将业务逻辑保持在域层内,更具体地说,将其保持在模型(聚合,实体和值对象)内,以避免以Anemic Domain Model结尾。某些类型的规则(例如访问控制,琐碎的数据类型验证等)本质上可能不会被视为业务规则,因此可以委派给应用程序层,但核心域规则不应泄漏到域外。

  

我更愿意为Manager属性传递一个UserId值对象,而不是一个用户集合

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

在提供外部信息以进行汇总时,有两种主要策略:

  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, ...);
              ...
          }
      }
      
    • 何时使用?这是最标准的方法,也是“最纯正的”方法(聚合从未执行间接IO)。我总是会首先考虑这种策略。

    • 要注意什么?与您自己的代码示例一样,将AR传递到他人的方法中可能很诱人,但是我会尽量避免使用它来防止对AR的意外突变。通过了AR实例,并且还避免了对超出所需合同的依赖。

  2. 将域服务传递到该域服务,该服务可用于自行查找数据。

    • 示例(伪代码):

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