Статьи

Практический рефакторинг PHP: замените код типа на класс

Сегодняшний сценарий будет знаком многим разработчикам, имеющим опыт работы с полями базы данных ENUM: он состоит из числового или фиксированного кода строкового типа, используемого для различения экземпляров класса.

Например, некоторые пользователи вашего сайта могут иметь U в поле приватной строки, чтобы отличить их как пользователей , в то время как некоторые администраторы могут иметь S в поле (для суперпользователя ). Или у вас может быть логическое поле, которое является ложным для неактивных пользователей и истинным для активных.

Этот тип полей является наследием процедурного кода. Вы можете иметь или не иметь код, который зависит от значения этого поля для работы. В этой первой статье о рефакторинге из кодов типов мы начнем с самой простой ситуации ; в этом случае поведение класса не зависит от кода типа, который является чистым элементом данных.

Зачем заменять код типа (даже если нет логики)?

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

Обычно коды типов — это просто скалярное поле, которое может содержать любое значение: установщики часто не проверяют, действительно ли это значение действительно. Например, установщик может разрешить установку не только U или S в поле user_type , но и других строк.

Заменив такое скалярное значение на экземпляр класса, мы получаем возможность подсказки типа в установщике . Более того, мы централизуем проверки в конструкторе или в методах фабрики нового класса: даже если поле используется повторно в другом месте, оно по-прежнему допускает тот же диапазон значений.

Наконец, мы прокладываем путь для перемещения кода в класс кода типа . Даже если код не зависит от значения, он может быть хорошо инкапсулирован в объект, если он может измениться в другое время относительно остальной части исходного класса. В нашем примере user / superuser добавление новых пользовательских типов приведет только к изменению меньшего класса UserType, не затрагивая больший класс User и проверяя каждую его строку.

меры

  1. Создайте новый класс , назвав его после кода типа. Класс должен иметь личное поле, скопированное полем исходного кода типа.
  2. Измените исходный класс, чтобы включить новый класс : в это время он должен создать новый объект, когда код задан, и извлечь значение из кода типа, когда это необходимо в получателе.
  3. Тесты все еще должны проходить, даже на уровне единиц исходного класса.
  4. В методах исходного класса, которые используют старый код, извлеките соответствующее количество логики в новом классе . Геттеры и сеттеры должны быть включены в эти модификации.
  5. Измените клиенты, чтобы они использовали новый интерфейс класса, распространяя новые объекты вне его.
  6. Проверьте состояние набора тестов: после добавления нового интерфейса как в исходном классе, так и в его клиентах он должен быть зеленого цвета.
  7. Удалите старый интерфейс и объявление нескольких значений кода.
  8. Проверьте набор тестов, чтобы убедиться, что вы закончили.

пример

В исходном состоянии мы используем N и G в качестве кодов типов для новичка и гуру. Эта эстетическая область, вероятно, изменится с опытом, поскольку пользователь становится более активным на сайте; но в этой первой статье мы рассматриваем это как ценность.

<?php
class ReplaceTypeCodeWithClass extends PHPUnit_Framework_TestCase
{
    public function testAnUserCanBeANewbieOrAGuru()
    {
        $user = new User();
        $this->assertEquals(User::NEWBIE, $user->getRank());
        $user->setRank(User::GURU);
        $this->assertEquals(User::GURU, $user->getRank());
    }
}

class User
{
    const NEWBIE = 'N';
    const GURU = 'G';

    private $rank = 'N';

    public function setRank($rank)
    {
        $this->rank = $rank;
    }

    public function getRank()
    {
        return $this->rank;
    }
}

Мы добавляем класс Rank, предоставляя также фабричные методы для дальнейшего использования.

class Rank
{
    private $code;

    public function __construct($code)
    {
        $this->code = $code;
    }

    public function getCode()
    {
        return $this->code;
    }

    public static function newbie()
    {
        return new self('N');
    }

    public static function guru()
    {
        return new self('G');
    }
}

Мы начинаем использовать Rank внутри класса: модульный тест, представляющий код внешнего клиента, не меняется и продолжает проходить.

<?php
class ReplaceTypeCodeWithClass extends PHPUnit_Framework_TestCase
{
    public function testAnUserCanBeANewbieOrAGuru()
    {
        $user = new User();
        $this->assertEquals(User::NEWBIE, $user->getRank());
        $user->setRank(User::GURU);
        $this->assertEquals(User::GURU, $user->getRank());
    }
}

class User
{
    const NEWBIE = 'N';
    const GURU = 'G';

    private $rank;

    public function __construct()
    {
        $this->rank = new Rank('N');
    }

    public function setRank($rank)
    {
        $this->rank = new Rank($rank);
    }

    public function getRank()
    {
        return $this->rank->getCode();
    }
}

class Rank
{
    private $code;

    public function __construct($code)
    {
        $this->code = $code;
    }

    public function getCode()
    {
        return $this->code;
    }

    public static function newbie()
    {
        return new self('N');
    }

    public static function guru()
    {
        return new self('G');
    }
}

Теперь мы также используем Rank в общедоступном интерфейсе, сохраняя старые имена методов. Мы также удаляем удаленные старые коды и делаем конструктор Rank закрытым. Наряду с подсказкой типа в setRank () этот шаг предотвращает установку недопустимых значений.

<?php
class ReplaceTypeCodeWithClass extends PHPUnit_Framework_TestCase
{
    public function testAnUserCanBeANewbieOrAGuru()
    {
        $user = new User();
        $this->assertEquals(Rank::newbie(), $user->getRank());
        $user->setRank(Rank::guru());
        $this->assertEquals(Rank::guru(), $user->getRank());
    }
}

class User
{
    private $rank;

    public function __construct()
    {
        $this->rank = Rank::newbie();
    }

    public function setRank(Rank $rank)
    {
        $this->rank = $rank;
    }

    public function getRank()
    {
        return $this->rank;
    }
}

class Rank
{
    private $code;

    private function __construct($code)
    {
        $this->code = $code;
    }

    public function getCode()
    {
        return $this->code;
    }

    public static function newbie()
    {
        return new self('N');
    }

    public static function guru()
    {
        return new self('G');
    }
}