php - DDD/CQRS/ES - 如何以及在何处实现防护

标签 php domain-driven-design cqrs

早上好,

我有一个模型,其中用户 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),因为它仍然可以防止大多数违规行为在低争用情况下发生。

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

  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 实例,并避免创建对超出需要的合约的依赖。

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

关于php - DDD/CQRS/ES - 如何以及在何处实现防护,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54476846/

相关文章:

php - PHP 中的 Mysql Union 未返回正确结果

OR 和 AND 条件的 PHP 查询生成器

c# - 在 webjob 和 web api 之间共享域 DLL?

architecture - 每个命令的事件溯源再水合?

php - 如何实现.htaccess盗链保护

php - jQuery AJAX/PHP/MySQL 实时过滤

c# - 域实体中的外键属性

java - 我应该在 'user of given id not exist' 时抛出 IllegalArgumentException 吗?

cqrs - CockroachDB 作为 Eventstore 是个好主意吗?

web-services - 从 CQRS 访问 Web 服务