Как использовать многоразовую проверку в 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());
    }

Другие вопросы по тегам