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

Met*_*end 8 php validation domain-driven-design dependency-injection 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
}
Run Code Online (Sandbox Code Playgroud)

示例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
}
Run Code Online (Sandbox Code Playgroud)

示例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);
}
Run Code Online (Sandbox Code Playgroud)

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

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

我开始考虑的最后一个例子是提供一个默认验证器,同时可以通过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;
}

}
Run Code Online (Sandbox Code Playgroud)

Gar*_*ing 1

例4!

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

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

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

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;
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,在下面,当您设置单元测试时,我们有一个 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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