我正在尝试将自己的头组合在一起。
永远不要使创建无效的ValueObject成为现实。每当提供的内容不足以创建有效的ValueObject时,valueObject构造函数就会失败。在我的示例中,只有在存在一个值时才能创建一个电子邮件address对象。到目前为止,一切都很好。
验证提供的EmailAddress的价值,这就是我开始怀疑原则的地方。我有四个例子,但我不能说哪个应该被视为最佳实践。
示例1是简单的:简单的构造函数,必需的参数"值"和一个单独的函数验证以保持代码清洁。所有验证代码都保留在班上,并且永远不会向外界使用。该课程只有一个目的:存储电子邮件address,并确保它永远不会是无效的。但是代码永远不会重复使用 - 我使用它创建一个对象,但仅此而已。
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使验证函数成为静态函数。该函数将永远不会更改类的状态,因此它是静态关键字的正确使用,并且它中的代码将永远无法将任何内容更改为从嵌入静态函数的类创建的任何实例。但是,如果我想重复使用代码,我可以调用静态函数。尽管如此,这对我来说很脏。
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注入构造器中验证器的覆盖物成为可能。但是,当您覆盖最重要的部分时,我开始怀疑一个简单的价值对象是多么良好:验证。
因此,任何人都有一个答案,哪种方式应最好地写这堂课,这对于像emailaddress这样的简单事物,或者更复杂的东西(例如条形码或签证卡或任何人想到的东西)都是正确的,而没有't违反ddd,di,oop,干燥,静态的错误使用等...
完整的代码:
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!
为什么?因为它是可测试的,简单的。
取决于您的验证器的实际功能(在某些情况下,验证器可能依赖API调用或对数据库的调用),可通过模拟完全测试可注射验证器。在我刚才提到的情况下,所有对方都无法测试,或者很难测试。
编辑:对于那些想知道依赖项注入方法有助于测试的人,然后考虑下面使用标准Akismet垃圾邮件检查库的注释validator类。
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;
}
}
}
}
现在,在下面,当您设置单元测试时,我们有一个commentValidatorock类,我们使用的是,我们有设置器可以手动更改我们可以拥有的2个输出bool,并且我们从上面的模拟'中具有2个功能'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。
第一个本能通常是最好的。您应该使用第一个选项。emailAddress是一个值对象。它可以在其他值对象或实体中重复使用。我不明白为什么您认为这是不可重复使用的。您可以拥有在其他有限上下文中使用的这些共同价值对象的"共享库"。只要小心您放在那里的东西即可。如果在概念上可能会有可能的话,他们将需要真正通用。
我认为,如果您使用单独的验证方法或将验证者移动到分开的类将是黄油并防止干燥
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());
}