Статьи

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

Иногда рефакторинг, который начинается с кодов типов, принимает обратное направление.

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

Чтобы упростить конструкцию, иерархия (или ее часть), которая изменяется только с этими значениями, может быть свернута в одном классе, где одно или несколько полей моделируют значения переменных.

Почему это дошло до этого?

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

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

Решение на основе полей допускает изменения :

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

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

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

меры

  1. Замените использование конструктора вызовом метода Factory для каждого из различных подклассов: таким образом, вы можете изменить конкретный класс для создания экземпляра в одном месте. Вы должны поместить эти фабричные методы в суперкласс, так как он будет единственным, кто переживет этот рефакторинг.
  2. Код, который ссылается на подклассы , например подсказки типов, должен ссылаться только на суперкласс, в который вы собираетесь их свернуть.
  3. Добавьте поля в суперкласс для каждого значения, обычно частного.
  4. Измените конструктор суперкласса на защищенную видимость и примите значения полей.
  5. Теперь подклассы должны использовать эту новую версию конструктора и передавать ей свои постоянные значения для полей: вы все еще создаете экземпляры различных конкретных классов.
  6. Реализуйте каждый постоянный метод в суперклассе , возвращая соответствующее поле; теперь метод может быть удален из подкласса. Повторение.
  7. Для каждого подкласса, если его конструктор имеет какую-либо дополнительную логику, вы можете встроить ее в соответствующий фабричный метод в суперклассе. Затем вы можете удалить подкласс.

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

пример

Мы начнем со случая двух специализаций ссылки (<a>) View Helper, объекта, производящего HTML-код. Наша иерархия состоит из класса Link с абстрактным методом для дифференцирования отрендеренного HTML и пары подклассов ExternalLink и InternalLink.

Некоторые коммиты назад эти подклассы отображались совершенно по-разному: например, один из них добавил target = «_ blank», или URL-адрес перенаправления позволяет подсчитывать клики по внешней ссылке.

Теперь классы стали практически одинаковыми.

<?php
class ReplaceSubclassWithFields extends PHPUnit_Framework_TestCase
{
    public function testInternalLinkShouldRender()
    {
        $a = new InternalLink('/posts/32', 'Last post');
        $this->assertEquals('<a href="/posts/32" class="internal">Last post</a>',
                            $a->__toString());
    }

    public function testExternalLinkShouldRender()
    {
        $a = new ExternalLink('http://www.google.com', 'Google');
        $this->assertEquals('<a href="http://www.google.com" class="external">Google</a>',
                            $a->__toString());
    }
}

abstract class Link
{
    private $href;
    private $text;

    public function __construct($href, $text)
    {
        $this->href = $href;
        $this->text = $text;
    }

    public function __toString()
    {
        return '<a href="' . $this->href 
             . '" class="' . $this->getCssClass() . '">'
             . $this->text . '</a>';
    }

    abstract protected function getCssClass();
}

class InternalLink extends Link
{
    public function getCssClass()
    {
        return 'internal';
    }
}

class ExternalLink extends Link
{
    public function getCssClass()
    {
        return 'external';
    }
}

Мы вводим фабричные методы в Link: теперь их должен вызывать весь клиентский код.

<?php
class ReplaceSubclassWithFields extends PHPUnit_Framework_TestCase
{
    public function testInternalLinkShouldRender()
    {
        $a = Link::internalLink('/posts/32', 'Last post');
        $this->assertEquals('<a href="/posts/32" class="internal">Last post</a>',
                            $a->__toString());
    }

    public function testExternalLinkShouldRender()
    {
        $a = Link::externalLink('http://www.google.com', 'Google');
        $this->assertEquals('<a href="http://www.google.com" class="external">Google</a>',
                            $a->__toString());
    }
}

abstract class Link
{
    private $href;
    private $text;

    public function __construct($href, $text)
    {
        $this->href = $href;
        $this->text = $text;
    }

    public static function internalLink($href, $text)
    {
        return new InternalLink($href, $text);
    }

    public static function externalLink($href, $text)
    {
        return new ExternalLink($href, $text);
    }

    public function __toString()
    {
        return '<a href="' . $this->href 
             . '" class="' . $this->getCssClass() . '">'
             . $this->text . '</a>';
    }

    abstract protected function getCssClass();
}

Мы добавляем поле $ cssClass, которое теперь оценивается различными значениями с помощью метода Factory (пропуская использование конструкторов в подклассах, которых нет). Теперь мы можем использовать новое поле в суперклассе, и нам больше не нужна логика в подклассах.

abstract class Link
{
    private $href;
    private $text;
    private $cssClass;

    public function __construct($href, $text, $cssClass)
    {
        $this->href = $href;
        $this->text = $text;
        $this->cssClass = $cssClass;
    }

    public static function internalLink($href, $text)
    {
        return new InternalLink($href, $text, 'internal');
    }

    public static function externalLink($href, $text)
    {
        return new ExternalLink($href, $text, 'external');
    }

    public function __toString()
    {
        return '<a href="' . $this->href 
             . '" class="' . $this->getCssClass() . '">'
             . $this->text . '</a>';
    }

    abstract protected function getCssClass();
}

Последний шаг — превратить Link в конкретный класс (это уже могло быть) и удалить ненужные подклассы.

class Link
{
    private $href;
    private $text;
    private $cssClass;

    public function __construct($href, $text, $cssClass)
    {
        $this->href = $href;
        $this->text = $text;
        $this->cssClass = $cssClass;
    }

    public static function internalLink($href, $text)
    {
        return new Link($href, $text, 'internal');
    }

    public static function externalLink($href, $text)
    {
        return new Link($href, $text, 'external');
    }

    public function __toString()
    {
        return '<a href="' . $this->href 
             . '" class="' . $this->cssClass . '">'
             . $this->text . '</a>';
    }
}

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

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

<?php
class ReplaceSubclassWithFields extends PHPUnit_Framework_TestCase
{
    public function testInternalLinkShouldRenderWithTheRightCssClass()
    {
        $a = new Link('/posts/32', 'Last post', 'myClass');
        $this->assertEquals('<a href="/posts/32" class="myClass">Last post</a>',
                            $a->__toString());
    }
}