Статьи

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


Эта статья является третьей и последней частью рефакторинга из кодов типов минисериалов.

  • Во-первых , мы видели случай, когда поведение значений кода типа не изменялось: код типа мог быть заменен одним классом.
  • Во-вторых , мы увидели случай, когда поведение изменилось : мы заменили разные коды типов подклассами исходного класса.
  • В третьем случае, который мы увидим сегодня, некоторое поведение зависит от кода типа, но расширение текущего класса невозможно (так как подклассы уже использовались или как выбор дизайна). Этот рефакторинг использует композицию вместо наследования.

Что отличается от двух других рефакторингов?

Мы можем сделать быстрое сравнение с двумя другими решениями.

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

Заменив код типа на подклассы, вы используете несколько подтипов для моделирования различного поведения, устраняя условные структуры, такие как ifs и switch. Но в этом рефакторинге они будут подтипами внешнего класса, а не исходного.

Созданный вами класс, представляющий код типа, является реализацией шаблона State или Strategy . В случае State объект часто меняет свой класс, возвращая новый экземпляр. В случае Стратегии объект изменяется редко, и разные реализации содержат разные алгоритмы или политики.

Зачем составлять объекты кода типа?

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

Кроме того, должно применяться одно или несколько из следующих условий:

  • подклассы недоступны из-за уже существующей иерархии или из-за отсутствия объяснений, так как имена классов неясны и обоснованы.
  • Жизненный цикл объекта не входит в управление , так что вы не можете создать экземпляр подклассы.
  • Код типа часто меняется , и восстановление правильного класса является беспорядком по техническим причинам (например, в вашей среде или в ORM).

меры

Шаги для применения этого рефакторинга аналогичны случаю «
Заменить код типа на подклассы» , но на этот раз они должны применяться к объекту «Состояние» или «Стратегия», а не к основному классу.

  1. Во-первых, вы должны самостоятельно инкапсулировать код типа.
  2. Затем создайте новый класс в качестве объекта State . Начните применять к нему предыдущий рефакторинг, что означает …
  3. Добавьте подкласс объекта State для каждого значения кода типа.
  4. GetTypeCode () в исходном классе должны быть переданы в государственный объект, который должен быть создан и сохранен в поле. Каждый подкласс должен переопределять метод, предоставляя свое собственное значение.

Теперь вы можете перемещать логику в различные подсостояния. Фактическое значение кода типа может быть удалено, если оно не используется для отображения или вообще не запрашивается.

пример

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

<?php
class ReplaceTypeCodeWithState extends PHPUnit_Framework_TestCase
{
    public function testAnUserCanBeANewbie()
    {
        $user = new User("Giorgio", User::NEWBIE);
        $this->assertEquals("Giorgio", $user->__toString());
    }

    public function testAnUserCanBeRegardedAsAGuru()
    {
        $user = new User("Giorgio", User::GURU);
        $this->assertEquals("ADMIN: Giorgio", $user->__toString());
    }
}

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

    private $name;
    private $rank;

    public function __construct($name, $rank)
    {
        $this->name = $name;
        $this->rank = $rank;
    }

    public function __toString()
    {
        if ($this->rank == self::GURU) {
            return "ADMIN: $this->name";
        }
        // self::NEWBIE
        return $this->name;
    }
}

Мы начнем с самоинкапсуляции
кода типа
ранга .

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

    private $name;
    private $rank;

    public function __construct($name, $rank)
    {
        $this->name = $name;
        $this->rank = $rank;
    }

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

    public function __toString()
    {
        if ($this->getRank() == self::GURU) {
            return "ADMIN: $this->name";
        }
        // self::NEWBIE
        return $this->name;
    }
}

Добавляем класс Rank.

class Rank
{
    public abstract function getCode();
}

Мы добавляем подклассы NewbieRank и GuruRank, а затем преобразуем приватное поле в экземпляр Rank вместо кода строкового типа.

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

    private $name;
    private $rank;

    public function __construct($name, Rank $rank)
    {
        $this->name = $name;
        $this->rank = $rank;
    }

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

    public function __toString()
    {
        if ($this->getRank() == self::GURU) {
            return "ADMIN: $this->name";
        }
        // self::NEWBIE
        return $this->name;
    }
}

class Rank
{
    public abstract function getCode();
}

class NewbieRank
{
    public function getCode()
    {
        return User::NEWBIE;
    }
}

class GuruRank
{
    public function getCode()
    {
        return User::GURU;
    }
}

Теперь мы можем переместить переменную логику __toString () в подклассы.
Статья обновлена: в подклассах Rank отсутствует ключевое слово extends.

<?php
class ReplaceTypeCodeWithState extends PHPUnit_Framework_TestCase
{
    public function testAnUserCanBeANewbie()
    {
        $user = new User("Giorgio", new NewbieRank);
        $this->assertEquals("Giorgio", $user->__toString());
    }

    public function testAnUserCanBeRegardedAsAGuru()
    {
        $user = new User("Giorgio", new GuruRank);
        $this->assertEquals("ADMIN: Giorgio", $user->__toString());
    }
}

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

    private $name;
    private $rank;

    public function __construct($name, Rank $rank)
    {
        $this->name = $name;
        $this->rank = $rank;
    }

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

    public function __toString()
    {
        return $this->rank->label() . $this->name;
    }
}

abstract class Rank
{
    public abstract function getCode();
    public abstract function label();
}

class NewbieRank extends Rank
{
    public function getCode()
    {
        return User::NEWBIE;
    }

    public function label()
    {
        return '';
    }
}

class GuruRank extends Rank
{
    public function getCode()
    {
        return User::GURU;
    }

    public function label()
    {
        return 'ADMIN: ';
    }
}

Переместив логику переменных в иерархию Rank, мы можем снова удалить метод getCode (), если он больше не требуется. Мы также можем превратить Rank в интерфейс, если он стабилизирован как чисто абстрактный класс без какого-либо кода.