【问题标题】:Data Mapper pattern, exceptions, and handling user-provided data数据映射器模式、异常和处理用户提供的数据
【发布时间】:2014-06-27 11:26:39
【问题描述】:

在应用Data Mapper 模式时,模型(在我的例子中为Domain Model)尽可能负责业务逻辑,而不是负责将实体保存到数据库的映射器。

构建一个单独的业务逻辑验证器来处理模型之外的用户提供的数据是否合理?

下面是一个例子,使用 PHP 语法。

假设我们有一个实体$person。假设该实体有一个属性surname,保存时不能为空。

用户为surname 输入了非法的空值。由于模型负责封装业务逻辑,我希望$person->surname = $surname; 在用户输入的$surname 为空字符串时以某种方式表示操作不成功。

在我看来,如果我们试图用非法值填充 $person 的属性之一,它应该抛出异常。

但是,from what I've read on exceptions “用户输入‘错误’输入也不例外:这是意料之中的。”这意味着不依赖异常来验证用户数据。

您建议如何解决这个问题,在让领域模型定义业务逻辑与不依赖领域模型在填充用户输入数据时抛出的异常之间取得平衡?

【问题讨论】:

    标签: php exception-handling datamapper domain-model


    【解决方案1】:

    域模型不一定是可以直接转换为数据库行的对象。 您的 Person 示例确实符合此描述,我喜欢将这样的对象称为实体(从 Doctrine 2 ORM 中采用)。 但是,就像 Martin Fowler 所描述的那样,域模型是包含行为和数据的任何对象。

    严格的解决方案

    对于您描述的问题,这是一个非常严格的解决方案:

    说您的Person 域模型(或实体)必须有名字和姓氏,并且可选地有婚前姓氏。这些必须是字符串,但为简单起见可以包含任何字符。 您希望强制只要存在这样的Person,就满足这些先决条件。该类将如下所示:

    class Person
    {
        /**
         * @var string
         */
        protected $firstname;
    
        /**
         * @var string
         */
        protected $lastname;
    
        /**
         * @var string|null
         */
        protected $maidenname;
    
        /**
         * @param  string      $firstname
         * @param  string      $lastname
         * @param  string|null $maidenname
         */
        public function __construct($firstname, $lastname, $maidenname = null)
        {
            $this->setFirstname($firstname);
            $this->setLastname($lastname);
            $this->setMaidenname($maidenname);
        }
    
        /**
         * @param string $firstname
         */
        public function setFirstname($firstname)
        {
            if (!is_string($firstname)) {
                throw new InvalidArgumentException('Must be a string');
            }
    
            $this->firstname = $firstname;
        }
    
        /**
         * @return string
         */
        public function getFirstname()
        {
            return $this->firstname;
        }
    
        /**
         * @param string $lastname
         */
        public function setLastname($lastname)
        {
            if (!is_string($lastname)) {
                throw new InvalidArgumentException('Must be a string');
            }
    
            $this->lastname = $lastname;
        }
    
        /**
         * @return string
         */
        public function getLastname()
        {
            return $this->lastname;
        }
    
        /**
         * @param string|null $maidenname
         */
        public function setMaidenname($maidenname)
        {
            if (!is_string($maidenname) or !is_null($maidenname)) {
                throw new InvalidArgumentException('Must be a string or null');
            }
    
            $this->maidenname = $maidenname;
        }
    
        /**
         * @return string|null
         */
        public function getMaidenname()
        {
            return $this->maidenname;
        }
    }
    

    如您所见,在不满足先决条件的情况下,您无法实例化 Person 对象(忽略反射)。 这是一件好事,因为每当您遇到Person 对象时,您都可以 100% 确定您正在处理的数据类型。

    现在您需要第二个域模型来处理用户输入,我们称它为PersonForm(因为它通常代表正在网站上填写的表格)。 它具有与Person 相同的属性,但盲目地接受任何类型的数据。 它还将有一个验证规则列表,一个像isValid() 这样使用这些规则来验证数据的方法,以及一个获取任何违规的方法。 我会把类的定义留给你想象:)

    最后,您需要一个控制器(或服务)将它们联系在一起。这是一些伪代码:

    class PersonController
    {
        /**
         * @param Request      $request
         * @param PersonMapper $mapper
         * @param ViewRenderer $view
         */
        public function createAction($request, $mapper, $view)
        {
            if ($request->isPost()) {
                $data = $request->getPostData();
    
                $personForm = new PersonForm();
                $personForm->setData($data);
    
                if ($personForm->isValid()) {
                    $person = new Person(
                        $personForm->getFirstname(),
                        $personForm->getLastname(),
                        $personForm->getMaidenname()
                    );
    
                    $mapper->insert($person);
    
                    // redirect
                } else {
                    $view->setErrors($personForm->getViolations());
                    $view->setData($data);
                }
            }
    
            $view->render('create/add');
        }
    }
    

    如您所见,PersonForm 用于拦截和验证用户输入。只有当该输入有效时,才会创建 Person 并将其保存在数据库中。

    业务规则

    这确实意味着某些业务逻辑将被复​​制:

    Person 中,您需要强制执行业务规则,但它可以简单地在出现问题时抛出异常。

    PersonForm 中,您将拥有应用相同规则的验证器,以防止无效的用户输入到达Person。但是在这里,那些验证器可以更高级。考虑诸如人为错误消息、违反第一条规则等问题。您还可以应用稍微改变输入的过滤器(例如将用户名小写)。

    换句话说:Person 将在低级别执行业务规则,而PersonForm 更多的是处理用户输入。

    更方便

    一种不太严格的方法,但可能更方便:

    限制在Person 中完成的验证以强制执行所需的属性,并强制执行属性的类型(字符串、int 等)。仅此而已。

    您还可以在Person 中获得约束列表。这些是业务规则,但没有实际的验证代码。所以只是一点配置而已。

    拥有一个Validator 服务,该服务能够接收数据以及约束列表。它应该能够根据约束验证该数据。您可能需要为每种类型的约束创建一个小的验证器类。 (看看Symfony 2 validator component)。

    PersonForm 可以注入Validator 服务,因此它可以使用该服务来验证用户输入。

    最后,有一个PersonManager 服务负责您想要在Person 上执行的任何操作(例如创建/更新/删除,可能还有注册/激活/等)。 PersonManager 需要 PersonMapper 作为依赖项。

    当您需要创建Person 时,您调用类似$personManager->create($userInput); 的调用将创建PersonForm,验证数据,创建Person(当数据有效时),并持久化@ 987654352@ 使用PersonMapper

    这里的关键是:

    您可以在所有这些类周围画一个圆圈,并将其称为您的“个人域”(DDD)。该域的接口(入口点)是PersonManager 服务。 您要对Person 执行的每个操作必须通过PersonManager

    如果您在应用程序中坚持这一点,那么在确保业务规则方面您应该是安全的 :)

    【讨论】:

    • 谢谢贾斯珀。不过,我正在为业务逻辑的位置而苦苦挣扎。在域模型模式中,模型控制业务逻辑。作为一个人为的例子(因为我想不出更现实的例子),假设如果名字是“John”,那么他们的姓可能不是“Brouwer”。当然PersonFormPerson 模型都必须检查并执行该逻辑?我看到的最简单的方法是从Person 中删除该检查,但是域模型不再定义业务约束。
    • 我已经更新了我的答案,希望这样更清楚。
    【解决方案2】:

    我认为您的 $person->surname = ''; 应该引发错误或异常的设计可以简化。

    返回一次错误

    您不想在分配每个值时一直捕获错误,您需要一个简单的一站式解决方案,例如查看当前值的$person->Valididate()。然后当你调用->Save() 函数时,它会自动先调用->Validate() 并简单地返回False。

    返回错误详情

    但返回 False,甚至返回错误代码通常是不够的:您想要 'who?为什么?'细节。所以让我们使用一个类实例来包含细节,我称之为ItemFieldErrors。它传递给 Save() 并且仅在 Save() 返回 False 时查看。

    public function Validate(&$itemFieldErrors = NULL, $aItem = NULL);
    

    试试这个完整的ItemFieldErrors 实现。一个数组就足够了,但我发现这更有条理、通用和自我记录。您总是可以在任何地方/以您喜欢的方式更智能地传递和解析错误详细信息,但通常(如果不总是......)只需输出asText() 摘要就可以了。

    /**
     * Allows a model to log absent/invalid fields for display to user.
     * Can output string like "Birthdate is invalid, Surname is missing"
     * 
     * Pass this to your Validate() model function.
     */
    class ItemFieldErrors
    {
      const FIELDERROR_MISSING = 1;
      const FIELDERROR_INVALID = 2;
    
      protected $itemFieldErrors = array();
    
      function __construct()
      {
        $this->Clear();
      }
    
      public function AddErrorMissing($fieldName)
      {
        $this->itemFieldErrors[] = array($fieldName, ItemFieldErrors::FIELDERROR_MISSING);
      }
    
      public function AddErrorInvalid($fieldName)
      {
        $this->itemFieldErrors[] = array($fieldName, ItemFieldErrors::FIELDERROR_INVALID);
      }
    
      public function ErrorCount()
      {
        $count = 0;
        foreach ($this->itemFieldErrors as $error) {
          $count++;
        }
        unset($error);
        return $count;
      }
    
      public function Clear()
      {
        $this->itemFieldErrors = array();
      }
    
      /**
       * Generate a human readable string to display to user.
       * @return string
       */
      public function AsText()
      {
        $s = '';
        $comma = '';
        foreach($this->itemFieldErrors as $error) {
          switch ($error[1]) {
            case ItemFieldErrors::FIELDERROR_MISSING:
              $s .= $comma . sprintf(qtt("'%s' is absent"), $error[0]);
              break;
            case ItemFieldErrors::FIELDERROR_INVALID:
              $s .= $comma . sprintf(qtt("'%s' is invalid"), $error[0]);
              break;
            default:
              $s .= $comma . sprintf(qtt("'%s' has unforseen issue"), $error[0]);
              break;
          }
          $comma = ', ';
        }
        unset($error);
        return $s;
      }
    }
    

    当然有$person->Save() 需要接收它,以便它可以将它传递给Validate()。在我的代码中,每当我从用户(表单提交)“加载”数据时,都会调用相同的 Validate(),而不仅仅是在保存时。

    模型会这样做:

    class PersonModel extends BaseModel {
    
      public $item = array();
    
      public function Validate(&$itemFieldErrors = NULL, $aItem = NULL) {
        // Prerequisites
        if ($itemFieldErrors === NULL) { $itemFieldErrors = new ItemFieldErrors(); }
        if ($aItem === NULL) { $aItem = $this->item; }
    
        // Validate
        if (trim($aItem['name'])=='')          { $itemFieldErrors->AddErrorMissing('name'); }
        if (trim($aItem['surname'])=='')       { $itemFieldErrors->AddErrorMissing('surname'); }
        if (!isValidDate($aItem['birthdate'])) { $itemFieldErrors->AddErrorInvalid('birthdate'); }
    
        return ($itemFieldErrors->ErrorCount() == 0);
      }
    
      public function Load()..
      public function Save()..
    }
    

    这个简单的模型将保存$item 中的所有数据,因此它只是将字段公开为$person->item['surname']

    【讨论】:

      【解决方案3】:

      您的域要求在创建人员时提供名字和姓氏。我通常处理这个问题的方法是验证输入模型,输入模型可能看起来像;

      class PersonInput
      {
        var $firstName;
        var $surname;
      
        public function isValid() { 
          return isset($this->firstName) && isset($this->surname);
        }
      }
      

      这确实是一个守卫,您也可以将这些规则放入您的客户端代码中以尝试防止这种情况发生,或者您可以从您的帖子中返回一个无效的人员消息。我不认为这是一个例外,更像是“预期的”,这就是我编写保护代码的原因。您现在进入您的域可能如下所示;

      public function createPerson(PersonInput $input) { 
        if( $input->isValid()) {
           $model->createPerson( $input->firstName, $input->surname);
      
           return 'success';
        } else {
           return 'person must comtain a valid first name and surname';
        }
      }
      

      这只是我的观点,以及我如何让我的验证逻辑远离域逻辑。

      【讨论】:

        【解决方案4】:

        我认为“用户输入‘错误’输入也不例外:这是意料之中的。”值得商榷...

        但是如果你不想抛出异常,为什么不创建一个 isValid() 或 getValidationErrors() 方法呢?

        如果有人试图将无效实体保存到数据库中,您可以抛出异常。

        【讨论】:

          猜你喜欢
          • 2012-11-23
          • 2013-11-07
          • 2010-09-17
          • 2013-06-13
          • 1970-01-01
          • 1970-01-01
          • 2021-01-01
          • 2013-10-24
          • 1970-01-01
          相关资源
          最近更新 更多