Статьи

Практический рефакторинг PHP: замена значения данных на объект

Одним из правил простого проектирования является необходимость минимизировать количество движущихся частей, таких как классы и методы, при условии, что тесты выполнены, и мы не принимаем дублирование или ощущаем отсутствие четкой концепции. Таким образом, правило, которое помогает простому дизайну, состоит в том, чтобы использовать примитивные типы, если к полю уже не прикреплено некоторое поведение: мы не создаем класс для имени пользователя или пароля пользователя; мы просто используем несколько строк.

Однако, по мере нашего прогресса, мы должны иметь возможность пересматривать наши решения с помощью рефакторинга: если поле получает некоторую логику, это поведение должно моделироваться не методами в содержащем классе, а новым объектом . Код в этом новом классе можно использовать повторно, в то время как содержащий объект будет меняться от случая к случаю, и в итоге вы будете дублировать одни и те же методы.

Преобразование скалярного значения в объект является сущностью рефакторинга «Заменить значение данных на объект». В большинстве случаев в результате получается объект значения или объект параметра: в то время как DDD использует объекты значения в качестве концепций на уровне домена, этот рефакторинг носит более общий характер и может применяться везде . Например, в проекте мы начали вводить объекты передачи данных для моделирования данных, отправляемых контроллером на уровень обслуживания.

Значения данных в PHP

В PHP все скалярные значения по своей природе являются значениями данных, поскольку они не могут содержать методы:

  • строка, целые числа и логические значения являются правильными скалярами.
  • массивы не являются скалярными в Perl или математическом смысле, но они все еще являются примитивным типом.

На границе мы находим несколько простых объектов, используемых в качестве контейнеров данных в PHP:

  • ArrayObjects.
  • SplHeap и другие структуры данных SPL.

Классы на границе могут содержать методы, но исходный класс недоступен для модификации, и необходимо ввести косвенность. «Локальное расширение»

меры

  1. Создайте новый класс : он должен содержать в качестве частного поля только значение, которое вы хотите заменить. Методы, которые вам немедленно нужны, должны быть выбраны между конструктором, получателем и установщиком (где это необходимо).
  2. Измените поле в содержащем классе. Обновите конструктор, чтобы также создать новый объект и заполнить поле, или принять инъекцию (более редкий случай).
  3. Обновите исходный метод получения, чтобы делегировать новый.
  4. Обновите исходный установщик, чтобы делегировать его новому (если он есть) или создать новый объект.
  5. Запускать тесты на функциональном уровне ; изменения следует распространить на этапы строительства, в то время как внешнее использование не должно сильно меняться.

пример

В исходном состоянии магические массивы передаются вокруг. Очень просто создать массив, в котором ключ отсутствует или вызывается неправильно.

<?php
class ReplaceDataValueWithObject extends PHPUnit_Framework_TestCase
{
    public function testUserCanSetANewPassword()
    {
        $userService = new UserService(/* other dependencies*/);
        $userService->newPassword(array(
            'userId' => 42,
            'oldPassword' => 'gismo',
            'newPassword' => 'supersecret',
            'repeatNewPassword' => 'supersecret'
        ));
        $this->markTestIncomplete('This refactoring is about the introduction of an object; it suffices that the test does not explode.');
    }
}

class UserService
{
    public function newPassword($changePasswordData)
    {
        /* it's not interesting to do something here */
    }
}

После введения расширения ArrayObject была обеспечена небольшая безопасность типов, и мы нашли место, чтобы использовать методы с небольшими затратами.

<?php
class ReplaceDataValueWithObject extends PHPUnit_Framework_TestCase
{
    public function testUserCanSetANewPassword()
    {
        $userService = new UserService(/* other dependencies*/);
        $userService->newPassword(new ChangePasswordCommand(array(
            'userId' => 42,
            'oldPassword' => 'gismo',
            'newPassword' => 'supersecret',
            'repeatNewPassword' => 'supersecret'
        )));
        $this->markTestIncomplete('This refactoring is about the introduction of an object; it suffices that the test does not explode.');
    }
}

class UserService
{
    public function newPassword(ChangePasswordCommand $changePasswordData)
    {
        /* it's not interesting to do something here */
    }
}

class ChangePasswordCommand extends ArrayObject
{
}

Мы добавляем методы для реализации логики на этом объекте; в этом случае логика проверки; в общем случае, любой вид кода, который не должен дублироваться различными клиентами.
Для более строгой реализации оберните массив или другую структуру данных (скаляры, объекты SPL) вместо расширения ArrayObject, поскольку вы получаете неизменность и инкапсуляцию (но для объектов такого типа требуется небольшая инкапсуляция.)

class ChangePasswordCommand extends ArrayObject
{
    public function __construct($data)
    {
        if (!isset($data['userId'])) {
            throw new Exception('User id is missing.');
        }
        parent::__construct($data);
    }

    public function getPassword()
    {
        if ($this['newPassword'] != $this['repeatNewPassword']) {
            throw new Exception('Password do not match.');
        }
        return $this['newPassword'];
    }
}

Однако, будучи рефакторингом, это менее инвазивный вид введения объектов, которые вы можете сделать, поскольку клиентский код все еще может использовать интерфейс ArrayAccess и обрабатывать объект как скалярный массив.