Статьи

Практический рефакторинг PHP: Извлечение иерархии

Предположим, у вас есть класс God, который полон операторов if и case или логических флагов. Многие рефакторинги пытаются разделить обязанности на более мелкие объекты: методы, подклассы или соавторы.

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

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

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

Пахнет

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

  • логические поля , используемые для решения, что делать.
  • Типы кодов , обобщение булевых полей на O (N) частных случаев.
  • Если / еще цепочки или переключатели , особенно в зависимости от состояния текущего объекта.

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

меры

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

пример

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

<?php
class ExtractHierarchy extends PHPUnit_Framework_TestCase
{
    public function testAnOrdinaryTopicDisplaysItsTitleAsNormalText()
    {
        $topic = new Topic('OOP in R');
        $this->assertEquals('<div class="title">OOP in R</div>', $topic->title());
    }

    public function testATopicInEvidenceDisplaysItselfAsStronger()
    {
        $topic = new Topic('OOP in R', true);
        $this->assertEquals('<div class="title"><strong>OOP in R</strong></div>', $topic->title());
    }
}

class Topic
{
    private $title;
    private $inEvidence;

    public function __construct($title, $inEvidence = false)
    {
        $this->title = $title;
        $this->inEvidence = $inEvidence;
    }

    public function title()
    {
        $title = '<div class="title">';
        if ($this->inEvidence) {
            $title .= "<strong>$this->title</strong>";
        } else {
            $title .= $this->title;
        }
        $title .= '</div>';
        return $title;
    }
}

Мы инкапсулируем логику создания, пока очень простую, в метод Factory и делаем конструктор закрытым. Мы создаем два подкласса и корректируем логику создания, чтобы больше не создавать экземпляры универсальной темы суперкласса.

<?php
class ExtractHierarchy extends PHPUnit_Framework_TestCase
{
    public function testAnOrdinaryTopicDisplaysItsTitleAsNormalText()
    {
        $topic = Topic::fromFields('OOP in R');
        $this->assertEquals('<div class="title">OOP in R</div>', $topic->title());
    }

    public function testATopicInEvidenceDisplaysItselfAsStronger()
    {
        $topic = Topic::fromFields('OOP in R', true);
        $this->assertEquals('<div class="title"><strong>OOP in R</strong></div>', $topic->title());
    }
}

class Topic
{
    private $title;
    private $inEvidence;

    public static function fromFields($title, $inEvidence = false)
    {
        if ($inEvidence) {
            return new InEvidenceTopic($title, $inEvidence);
        } else {
            return new OrdinaryTopic($title, $inEvidence);
        }
    }

    private function __construct($title, $inEvidence)
    {
        $this->title = $title;
        $this->inEvidence = $inEvidence;
    }

    public function title()
    {
        $title = '<div class="title">';
        if ($this->inEvidence) {
            $title .= "<strong>$this->title</strong>";
        } else {
            $title .= $this->title;
        }
        $title .= '</div>';
        return $title;
    }
}

class OrdinaryTopic extends Topic
{
}

class InEvidenceTopic extends Topic
{
}

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

class Topic
{
    private $title;
    private $inEvidence;

    public static function fromFields($title, $inEvidence = false)
    {
        if ($inEvidence) {
            return new InEvidenceTopic($title, $inEvidence);
        } else {
            return new OrdinaryTopic($title, $inEvidence);
        }
    }

    private function __construct($title, $inEvidence)
    {
        $this->title = $title;
        $this->inEvidence = $inEvidence;
    }

    public function title()
    {
        $title = '<div class="title">';
        $title .= $this->decoratedTitleText();
        $title .= '</div>';
        return $title;
    }

    protected function decoratedTitleText()
    {
        if ($this->inEvidence) {
            return "<strong>$this->title</strong>";
        } else {
            return $this->title;
        }
    }
}

Мы копируем его в подклассы:

class OrdinaryTopic extends Topic
{
    protected function decoratedTitleText()
    {
        if ($this->inEvidence) {
            return "<strong>$this->title</strong>";
        } else {
            return $this->title;
        }
    }
}

class InEvidenceTopic extends Topic
{
    protected function decoratedTitleText()
    {
        if ($this->inEvidence) {
            return "<strong>$this->title</strong>";
        } else {
            return $this->title;
        }
    }
}

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

class OrdinaryTopic extends Topic
{
    protected function decoratedTitleText()
    {
        return $this->title;
    }
}

class InEvidenceTopic extends Topic
{
    protected function decoratedTitleText()
    {
        return "<strong>$this->title</strong>";
    }
}

Теперь мы можем удалить тело исходного метода изменчивости (но не сигнатуру) и объявить реферат базового класса. Дальнейшее упрощение также удаляет поле $ this-> inEvidence.

abstract class Topic
{
    protected $title;

    public static function fromFields($title, $inEvidence = false)
    {
        if ($inEvidence) {
            return new InEvidenceTopic($title);
        } else {
            return new OrdinaryTopic($title);
        }
    }

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

    public function title()
    {
        $title = '<div class="title">';
        $title .= $this->decoratedTitleText();
        $title .= '</div>';
        return $title;
    }

    /**
     * @return string
     */
    abstract protected function decoratedTitleText();
}