Статьи

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

Это вторая часть в рефакторинге из мини- серии кодов типов : коды типов — это скалярные поля, которые могут принимать конечное число значений.

Сегодня предполагается , что код типа влияет на поведение класса : в зависимости от значения поля выполняется другой код. Как правило, код выбирается через if () или другую управляющую структуру, такую ​​как select () или ?: . Любой код, который проверяет значение кода типа, является подозрительным.
На этот раз мы не можем выполнить рефакторинг путем извлечения одного класса, потому что мы просто переместим if в этот класс, который должен был бы охватить все различные случаи.

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

Почему подкласс?

Этот рефакторинг предпочитает полиморфизм структурам управления: он соблюдает принцип единой ответственности , разделяя код, включаемый различными значениями кода типа.

В следующий раз, когда вы добавите новое значение кода типа, вы добавите класс, не касаясь уже существующих ( Open Closed Principle ).

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

Когда вы не можете применить этот рефакторинг

Фаулер приводит некоторые случаи, когда этот рефакторинг не может быть применен (но не отчаивайтесь: есть альтернативы.)

Простой случай, когда код типа меняется очень часто . Тем не менее, я обнаружил, что если он изменяется из-за редкого перехода состояния, вы все равно можете использовать решение наследования, заставляя инкриминируемый метод возвращать новый экземпляр (например, InactiveUser :: activ () возвращает экземпляр ActiveUser). Это сложнее сделать, когда жизненный цикл объектов управляется не только сборкой мусора, но и внешними ресурсами, такими как база данных, доступ к которой осуществляется через ORM (необходимо, чтобы объекты представляли одну и ту же строку / документ / блоб и проверяли висячие ссылки .)

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

меры

  1. Прежде всего, само кодирование должно выполняться в коде типа , что приводит к защищенному методу getTypeCode ().
  2. Для каждого значения кода типа должен быть создан подкласс , который переопределяет метод getTypeCode () жестко закодированным.
  3. Далее мы должны заняться созданием : если код типа передается в конструктор, код создания должен быть перемещен в метод Factory, который может вернуть экземпляр правого подкласса. Единственные операторы if () останутся здесь пока.
  4. На этом этапе тесты должны быть зелеными. Логика все еще запутана в исходном классе, но различные подклассы решают, какой тип кода указать.
  5. Теперь мы можем удалить поле кода типа ; getTypeCode () может стать абстрактным, так как все подклассы предоставят его.
  6. Проверьте набор тестов еще раз.

Теперь вы можете перемещать логику в подклассы каждый раз, когда она специфична для определенного значения кода типа. В идеальном случае вы также сможете удалить метод getTypeCode (), когда закончите.

пример

Мы начинаем с той же пользовательской специализации предыдущего примера; однако на этот раз результат __toString () зависит от значения кода типа.

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

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

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

    protected $name;

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

    public static function newUser($name, $rank)
    {
        if ($rank == self::GURU) {
            return new Guru($name);
        }
        return new Newbie($name);
    }

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

class Guru extends User
{
    protected function getRank()
    {
        return self::GURU;
    }

    public function __toString()
    {
        return "ADMIN: $this->name";
    }
}

class Newbie extends User
{
    protected function getRank()
    {
        return self::NEWBIE;
    }

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

Во-первых, мы самостоятельно инкапсулируем код типа с помощью getRank ():

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;
    }
}

Затем мы добавляем два подкласса Newbie и Guru, которые переопределяют getRank (). Мы должны изменить процесс создания с конструктора на фабричный метод, который будет централизовать ifs в одном месте.

Мы также соответствующим образом модифицируем тестовый код, вызывая метод Factory.

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

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

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

    protected $name;
    private $rank;

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

    public static function newUser($name, $rank)
    {
        if ($rank == self::GURU) {
            return new Guru($name, null);
        }
        return new Newbie($name, null);
    }

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

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

class Guru extends User
{
    protected function getRank()
    {
        return self::GURU;
    }
}

class Newbie extends User
{
    protected function getRank()
    {
        return self::NEWBIE;
    }
}

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

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

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

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

    protected $name;

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

    public static function newUser($name, $rank)
    {
        if ($rank == self::GURU) {
            return new Guru($name);
        }
        return new Newbie($name);
    }

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

class Guru extends User
{
    protected function getRank()
    {
        return self::GURU;
    }

    public function __toString()
    {
        return "ADMIN: $this->name";
    }
}

class Newbie extends User
{
    protected function getRank()
    {
        return self::NEWBIE;
    }

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

Если код не нужен для отображения, мы также можем удалить getRank (), как только вся логика подкласса будет перемещена вниз в иерархии.