9

我正在尝试结合一些技术。

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

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

示例 1 很简单:只是一个构造函数、一个必需的参数“值”和一个单独的函数 validate 以保持代码清洁。所有的验证码都保留在类中,并且永远不会被外界使用。该类只有一个目的:存储电子邮件地址,并确保它永远不会是无效的。但是代码永远不会可重用——我用它创建了一个对象,仅此而已。

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 有多好。

所以,任何人都有一个答案,应该以哪种方式编写这个类最好,这对于像电子邮件地址这样简单的东西,或者像条形码或签证卡这样更复杂的东西或任何人们可能想到的东西都是正确的,并且不违反 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;
}

}
4

3 回答 3

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 类,我们有设置器来手动更改我们可以拥有的 2 个输出布尔值,并且我们拥有从上面模拟到的 2 个函数无需通过 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。

于 2014-01-22T16:10:47.223 回答
1

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

于 2016-02-25T20:42:59.390 回答
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());
    }

于 2020-09-07T00:11:05.060 回答