Статьи

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

После Pull Up Field и Pull Up Method мы рассмотрим сегодня последний из этой категории рефакторингов: Pull Up Constructor Body.

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

Почему конструктор — это не просто метод?

Этот рефакторинг отличается от Pull Up Method, потому что он использует особую семантику, такую ​​как вызовы parent . Более того, в некоторых языках конструкторы не наследуются; в PHP они есть, поэтому аспект наследования не важен.

Разница между извлечением метода и конструктора заключается в том, что в последнем случае существует стандартный способ разделения общего кода (для подтягивания) и конкретного (для оставления в подклассах). Стандартная форма — родительский конструктор, плюс переопределяющий конструктор, который вызывает parent :: __ construct () , скрывая его в процессе. Существуют альтернативы переопределению, такие как абстрактные методы init (), но они не фиксируют все дополнительные особые параметры, которые могут понадобиться во время создания экземпляра.

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

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

PHP Fatal error:  Declaration of Subclass::method() must be compatible with that of Superclass::method() in ...

Как и во всех рефакторингах Pull Up, благодаря наследованию клиентский код не должен изменяться.

меры

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

  1. Определите конструктор в суперклассе (изначально пустой и не вызванный).
  2. Переместите общий код , который должен быть в начале конструктора, вверх в суперклассе.
  3. Вызовите конструктор суперкласса в качестве первого шага каждого конструктора подкласса.

Это соглашение C ++, чтобы сохранить вызов родительского конструктора в качестве первого шага (в C ++ по умолчанию вызов выполняется автоматически.) Деструкторы, которые используются редко, имеют противоположную семантику.

пример

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

<?php
class PullUpConstructorBody extends PHPUnit_Framework_TestCase
{
    public function testAPostShowsItsAuthor()
    {
        $post = new Post("Hello, world!", "giorgiosironi");
        $this->assertEquals("Hello, world! -- @giorgiosironi",
                            $post->__toString());
    }

    public function testALinkShowsItsAuthor()
    {
        $link = new Link("http://en.wikipedia.com", "giorgiosironi");
        $this->assertEquals("<a href=\"http://en.wikipedia.com\">http://en.wikipedia.com</a> -- @giorgiosironi",
                            $link->__toString());
    }
}

abstract class NewsFeedItem
{
    /**
     * @var string  references the author's Twitter username
     */
    protected $author;

    /**
     * @return string   an HTML printable version
     */
    public function __toString()
    {
        return $this->displayedText() . " -- $this->author";
    }

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

class Post extends NewsFeedItem
{
    private $text;

    public function __construct($text, $author)
    {
        $this->text = $text;
        $this->author = '@' . ltrim($author, '@');
    }

    protected function displayedText()
    {
        return $this->text;
    }
}

class Link extends NewsFeedItem
{
    private $url;

    public function __construct($url, $author)
    {
        $this->url = $url;
        $this->author = '@' . ltrim($author, '@');
    }

    protected function displayedText()
    {
        return "<a href=\"$this->url\">$this->url</a>";
    }
}

Мы определяем конструктор, который на данный момент переопределен и невидим:

abstract class NewsFeedItem
{
    /**
     * @var string  references the author's Twitter username
     */
    protected $author;

    public function __construct()
    {
    }

    /**
     * @return string   an HTML printable version
     */
    public function __toString()
    {
        return $this->displayedText() . " -- $this->author";
    }

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

Мы перемещаем общий код внутри конструктора суперкласса. Тесты сейчас не проходят:

abstract class NewsFeedItem
{
    /**
     * @var string  references the author's Twitter username
     */
    protected $author;

    public function __construct($author)
    {
        $this->author = '@' . ltrim($author, '@');
    }

    /**
     * @return string   an HTML printable version
     */
    public function __toString()
    {
        return $this->displayedText() . " -- $this->author";
    }

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

class Post extends NewsFeedItem
{
    private $text;

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

    protected function displayedText()
    {
        return $this->text;
    }
}

class Link extends NewsFeedItem
{
    private $url;

    public function __construct($url, $author)
    {
        $this->url = $url;
    }

    protected function displayedText()
    {
        return "<a href=\"$this->url\">$this->url</a>";
    }
}

Чтобы снова выполнить тест, нам нужно вызвать родительский конструктор:

class Post extends NewsFeedItem
{
    private $text;

    public function __construct($text, $author)
    {
        parent::__construct($author);
        $this->text = $text;
    }

    protected function displayedText()
    {
        return $this->text;
    }
}

class Link extends NewsFeedItem
{
    private $url;

    public function __construct($url, $author)
    {
        parent::__construct($author);
        $this->url = $url;
    }

    protected function displayedText()
    {
        return "<a href=\"$this->url\">$this->url</a>";
    }
}

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

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