Статьи

Практический рефакторинг PHP: Push Down Field

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

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

Почему нажимать вниз, а не вверх?

Этот рефакторинг является противоположностью Pull Up Field : поле переносится в несколько подклассов вместо объединения нескольких полей в один.

Существует более общая тема, представленная методами Push Down и Push Down Field: каждый рефакторинг является двунаправленным, и вы можете идти в том или ином направлении в зависимости от вашей ситуации. Обычно одно направление направлено на устранение дублирования, в то время как другое временно создает его до того, как дублированные элементы станут отличаться друг от друга (можно дублировать поле, чтобы копии развивались независимо). Для крупномасштабных рефакторингов одно направление увеличивает количество элементов в дизайне, а другое уменьшает их, чтобы упростить общую картину.

Что касается Push Down Field, легко проверить выполнимость: поля обычно имеют очень ограниченную область действия, которая может быть закрытой или защищенной (и никогда не публичной в ООП):

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

меры

Шаги очень похожи на шаги для нажатия на метод * link *:

  1. объявить поле во всех подклассах .
  2. Удалите поле из суперкласса .
  3. Убедитесь, что тесты зеленые.
  4. Удалите поле из подклассов , по одному за раз.
  5. Проверьте еще раз, что тесты зеленые.

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

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

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

пример

В исходном состоянии мы видим два подкласса Post и Link. Однако $ url необходим только в объектах Link, и мы хотим переместить его только в этот класс.

<?php
class PushDownField 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("/posts/php-refactoring", "giorgiosironi");
        $this->assertEquals("<a href=\"/posts/php-refactoring\">/posts/php-refactoring</a> -- @giorgiosironi",
                            $link->__toString());
    }
}

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

    /**
     * @var string
     */
    protected $url;

    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)
    {
        parent::__construct($author);
        $this->text = $text;
    }

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

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

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

Мы копируем $ url в различные подклассы, не изменяя внешний интерфейс этих объектов (поскольку суперкласс является абстрактным, любой объект в иерархии является экземпляром Post или Link.)

class Post extends NewsFeedItem
{
    private $text;

    /**
     * @var string
     */
    protected $url;

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

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

class Link extends NewsFeedItem
{
    /**
     * @var string
     */
    protected $url;

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

    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($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)
    {
        parent::__construct($author);
        $this->text = $text;
    }

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

Удаление поля из Link не вызовет ошибок, но сделает поле общедоступным. В целях документирования и инкапсуляции лучше оставить эту копию на месте, поскольку мы ясно видим, что к ней будет обращаться код из класса Link.