如何在ValueObject中使用可重用验证

时间:2014-01-21 20:58:51

标签: php validation dependency-injection domain-driven-design value-objects

我正试图结合一些技巧。

似乎很好的做法是永远不可能创建无效的ValueObject。只要提供的内容不足以创建有效的ValueObject,ValueObject构造函数就会失败。在我的示例中,只有存在值时才能创建EmailAddress对象。到目前为止,非常好。

验证所提供的电子邮件地址的价值,这就是我开始怀疑原则的地方。我有四个例子,但我不知道哪一个应该被认为是最好的做法。

示例1很简单:只需构造函数,必需参数“value”和单独的函数验证以保持代码清洁。所有验证代码都保留在类中,并且永远不会对外界可用。该类只有一个目的:存储emailaddress,并确保它永远不会是无效的。但代码永远不会重复使用 - 我用它创建了一个对象,但就是这样。

public function __construct ($value)
{
    if ( $this->validate($value) )
    {
        throw new \ValidationException('This is not an emailaddress.');
    }
    $this->value = $value;
}

protected function validate ($value)
{
    return is_string($value); // Wrong function, just an example
}

示例2使validate函数成为静态函数。该函数永远不会改变类的状态,因此它正确使用了static关键字,并且其中的代码永远不能将任何内容更改为嵌入静态函数的类创建的任何实例。但是如果我想重用代码,我可以调用静态函数。不过,这让我觉得很脏。

public function __construct ($value)
{
    if ( $self::validate($value) )
    {
        throw new \ValidationException('This is not an emailaddress.');
    }
    $this->value = $value;
}

public static function validate ($value)
{
    return is_string($value); // Wrong function, just an example
}

示例3介绍了另一个类,在我的对象体内硬编码。另一个类是一个包含验证代码的验证类,因此创建了一个可以随时随地使用验证类的类。类本身是硬编码的,这也意味着我在该验证类上创建了一个依赖,它应该总是在附近,并且不是通过依赖注入注入的。可以说,硬编码验证器与在对象中嵌入完整代码一样糟糕,但另一方面:DI很重要,这样就必须创建一个新类(扩展或简单地重写)只需更改依赖关系。

public function __construct ($value)
{
    if ( $this->validate($value) )
    {
        throw new \ValidationException('This is not an emailaddress.');
    }
    $this->value = $value;
}

protected function validate ($value)
{
    $validator = new \Validator();
    return $validator->validate($value);
}

示例4再次使用验证器类,但将其放在构造函数中。因此,在创建类之前,我的ValueObject需要一个已经存在并创建的验证器类,但是可以轻松覆盖验证器。但是对于一个简单的ValueObject类来说,在构造函数中具有这样的依赖性有多好,因为唯一真正重要的是值,如果电子邮件是正确的,那么知道如何处理以及在何处处理它并不应该让我担心一个正确的验证器。

public function __construct ($value, \Validator $validator)
{
    if ( $validator->validate($value) )
    {
        throw new \ValidationException('This is not an emailaddress.');
    }
    $this->value = $value;
}

我开始考虑的最后一个例子是提供一个默认验证器,同时可以通过DI为构造函数中的验证器注入覆盖。但是当我覆盖最重要的部分时,我开始怀疑一个简单的ValueObject有多好:验证。

所以,任何人都应该以最好的方式写出这个课程的答案,这对于像电子邮件地址这样简单的东西,或者像条形码或签证卡或任何人可能想到的更复杂的东西都是正确的,并且不会t违反DDD,DI,OOP,DRY,错误使用静电等等......

完整的代码:

class EmailAddress implements \ValueObject
{

protected $value = null;

// --- --- --- Example 1

public function __construct ($value)
{
    if ( $this->validate($value) )
    {
        throw new \ValidationException('This is not an emailaddress.');
    }
    $this->value = $value;
}

protected function validate ($value)
{
    return is_string($value); // Wrong function, just an example
}

// --- --- --- Example 2

public function __construct ($value)
{
    if ( $self::validate($value) )
    {
        throw new \ValidationException('This is not an emailaddress.');
    }
    $this->value = $value;
}

public static function validate ($value)
{
    return is_string($value); // Wrong function, just an example
}

// --- --- --- Example 3

public function __construct ($value)
{
    if ( $this->validate($value) )
    {
        throw new \ValidationException('This is not an emailaddress.');
    }
    $this->value = $value;
}

protected function validate ($value)
{
    $validator = new \Validator();
    return $validator->validate($value);
}

// --- --- --- Example 4

public function __construct ($value, \Validator $validator)
{
    if ( $validator->validate($value) )
    {
        throw new \ValidationException('This is not an emailaddress.');
    }
    $this->value = $value;
}

}

3 个答案:

答案 0 :(得分:1)

例4!

为什么呢?因为它是可测试的,简单明了。

根据您的验证器实际执行的操作(在某些情况下,您的验证器可能依赖于API调用或对数据库的调用),可注入验证器可通过模拟完全测试。所有其他的都要么在我刚才提到的情况下测试,要么难以测试。

编辑:对于那些想知道依赖注入方法如何帮助测试的人,请考虑下面使用标准Akismet垃圾邮件检查库的CommentValidator类。

class CommentValidator {
    public function checkLength($text) {
        // check for text greater than 140 chars
        return (isset($text{140})) ? false : true;
    }

    public function checkSpam($author, $email, $text, $link) {
        // Load array with comment data.
        $comment = array(
                        'author' => $author,
                        'email' => $email,
                        'website' => 'http://www.example.com/',
                        'body' => $text,
                        'permalink' => $link
                );

        // Instantiate an instance of the class.
        $akismet = new Akismet('http://www.your-domain.com/', 'API_KEY', $comment);

        // Test for errors.
        if($akismet->errorsExist()) { // Returns true if any errors exist.
            if($akismet->isError('AKISMET_INVALID_KEY')) {
                    return true;
            } elseif($akismet->isError('AKISMET_RESPONSE_FAILED')) {
                    return true;
            } elseif($akismet->isError('AKISMET_SERVER_NOT_FOUND')) {
                    return true;
            }
        } else {
            // No errors, check for spam.
            if ($akismet->isSpam()) {
                    return true;
            } else {
                    return false;
            }
        }
    }
}

现在下面,当你设置单元测试时,我们有一个我们使用的CommentValidatorMock类,我们有setter来手动更改我们可以拥有的2个输出bool,并且我们有2个函数来自mock' d直到输出任何我们想要的东西而不必通过Akismet API。

class CommentValidatorMock {
    public $lengthReturn = true;
    public $spamReturn = false;

    public function checkLength($text) {
        return $this->lengthReturn;
    }

    public function checkSpam($author, $email, $text, $link) {
        return $this->spamReturn;
    }

    public function setSpamReturn($val) {
        $this->spamReturn = $val;
    }

    public function setLengthReturn($val) {
        $this->lengthReturn = $val;
    }
}

如果您对单元测试非常认真,那么您需要使用DI。

答案 1 :(得分:0)

第一直觉通常是最好的。您应该使用第一个选项。 EmailAddress是一个值对象。它可以在其他值对象或实体中重用。我不明白为什么你认为它不可重复使用。您可以在其他有界上下文中使用这些常用值对象的“共享库”。小心你放在那里。如果这在概念上是可能的话,它们将需要真正的通用。

答案 2 :(得分:0)

我认为,如果您使用单独的验证方法或将验证器移到单独的类上,那将是黄油,并且会阻止DRY


    class EmailAddress{
      protected $value;
      public function __construct ($value)
      {
        $this->value = \validateEmailAddress($value);
      }
    }

    function validateEmailaddress(string $value) : string  
    {
      if(!is_string($value)){
        throw new \ValidationException('This is not an emailaddress.');
      } // Wrong function, just an example
      return $value;
    }

    //OR for strict OOP people
    final class VOValidator{
      private function __construct(){}
      public static function validateEmailaddress(string $input): string{...}
    }


    //I will prefer even go far and use Either from (FP monads) 

    interface ValueObejctError {}
    class InvalidEmail implements ValueObjectError {}

    function validateEmailaddress(string $input): Either { 
    // it will be better if php supported generic so using Either<InvalidaEmail, string> is more readable but unfortunately php has no generic types, maybe in future
      return is_string($input) 
        ? new Right($input)
        : new Left(new InvalidEmail());
    }