Как использовать многоразовую проверку в ValueObject
Я пытаюсь объединить некоторые методы.
Кажется хорошей практикой никогда не делать невозможным создание объекта ValueObject. Поэтому конструктор ValueObject должен завершиться ошибкой, если предоставленный контент недостаточно хорош для создания допустимого ValueObject. В моих примерах объект EmailAddress может быть создан только при наличии значения. Все идет нормально.
Подтверждая ценность предоставленного адреса электронной почты, я начинаю сомневаться в принципах. У меня есть четыре примера, но я не могу сказать, какой из них следует считать лучшей практикой.
Пример 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
}
Пример 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 снова используется класс validator, но он помещается в конструктор. Таким образом, моему 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, ООП, СУХОЕ, неправильное использование статического и так далее...
Полный код:
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;
}
}
2 ответа
Пример 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;
}
}
}
}
И теперь ниже, когда вы настраиваете свои модульные тесты, у нас есть класс CommentValidatorMock, который мы используем вместо этого, у нас есть сеттеры для ручного изменения 2 выходных булев, которые мы можем иметь, и у нас есть 2 функции сверху mock'd до выводить все, что мы хотим, без необходимости проходить через API Akismet.
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());
}