Статьи

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

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

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

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

Почему композиция?

Устранение дублирования через наследование представляет некоторые проблемы.

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

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

Третья проблема связана с модульным тестированием и дублированием тестового кода . Должны ли мы проверять только поведение подклассов? Или мы должны проверить также унаследованные функции? В последнем случае мы продублируем тестовый код.

Наследование и делегирование (также называемое составом) являются двумя основными отношениями между классами в ООП. Они эквивалентны с теоретической, функциональной точки зрения, но так же, как и машина Тьюринга или язык пробелов .

меры

  1. Создайте поле в подклассе и инициализируйте его как $ this. Он будет содержать сотрудника.
  2. Измените методы в подклассе, чтобы использовать поле делегата. Методы, которые наследуются, могут потребоваться для делегирования родителю .
  3. Удалите объявление подкласса и замените делегат новым экземпляром суперкласса.

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

пример

Мы начинаем с конца примера метода Pull Up: мы хотим преобразовать суперкласс NewsFeedItem в соавтора с таким же поведением.

<?php
class ReplaceInheritanceWithDelegation 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 = $author;
    }

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

class Link extends NewsFeedItem
{
    private $url;

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

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

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

class Post extends NewsFeedItem
{
    private $text;

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

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

class Link extends NewsFeedItem
{
    private $url;

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

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

    public function __toString()
    {
        return parent::__toString();
    }
}

Определение имени для соавтора является важным шагом. Очень вероятно, что он изменится относительно имени, которое следует за LSP и используется для суперкласса. Мы выбираем «Формат», поскольку родительские модели позволяют печатать поля автора и содержимого.

Мы также извлекаем метод display () в суперклассе, чтобы разделить поведение форматирования от проводки до полей. Мы планируем использовать display () в качестве соавтора, в то время как __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->display($this->displayedText(), $this->author);
    }

    public function display($text, $author)
    {
        return "$text -- $author";
    }

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

class Post extends NewsFeedItem
{
    private $text;
    private $format;

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

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

    public function __toString()
    {
        return parent::__toString();
    }
}

class Link extends NewsFeedItem
{
    private $url;
    private $format;

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

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

    public function __toString()
    {
        return parent::__toString();
    }
}

We can start using the delegate instead of parent, and of relying on inheritance. __toString() is the only point where we have to intervene:

class Post extends NewsFeedItem
{
    private $text;
    private $format;

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

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

    public function __toString()
    {
        return $this->format->display($this->displayedText(), $this->author);
    }
}

class Link extends NewsFeedItem
{
    private $url;
    private $format;

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

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

    public function __toString()
    {
        return $this->format->display($this->displayedText(), $this->author);
    }
}

Now we can eliminate abstract and the abstract method in the superclass, plus the extends keyword in the subclasses. This means now $this->format would be initialized to an instance of TextSignedByAuthorFormat, which is the new name for NewsFeedItem. We also have to push down $this->author.

class TextSignedByAuthorFormat
{
    /**
     * @return string   an HTML printable version
     */
    public function __toString()
    {
        return $this->display($this->displayedText(), $this->author);
    }

    public function display($text, $author)
    {
        return "$text -- $author";
    }
}

class Post
{
    private $text;
    private $author;
    private $format;

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

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

    public function __toString()
    {
        return $this->format->display($this->displayedText(), $this->author);
    }
}

class Link
{
    private $url;
    private $author;
    private $format;

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

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

    public function __toString()
    {
        return $this->format->display($this->displayedText(), $this->author);
    }
}

Finally, we can simplify part of the code. We delete the __toString() on TextSignedByAuthorFormat which is dead code; and inline the displayedMethod() on Post, which served the inheritance-based solution but now is an unnecessary indirection.

class TextSignedByAuthorFormat
{
    public function display($text, $author)
    {
        return "$text -- $author";
    }
}

class Post
{
    private $text;
    private $author;
    private $format;

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

    public function __toString()
    {
        return $this->format->display($this->text, $this->author);
    }
}

class Link
{
    private $url;
    private $author;
    private $format;

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

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

    public function __toString()
    {
        return $this->format->display($this->displayedText(), $this->author);
    }
}

There are many further steps we could make:

  • inject the TextSignedByAuthorFormat object. Consequently, if the logic in the collaborator expands we can refactor tests to use a Test Double.
  • Move $this->author into the format.
  • Apply Extract Interface (Format should be the name), to be able to support multiple output formats. Another implementation could place a link on the author too, or could strip all or some of the tags for displaying in a RSS or in a tweet.