Когда подкласс нарушает принцип подстановки Лискова или использует только часть суперкласса, это предупреждающий знак того, что композиция может упростить конструкцию.
Рефакторинг в композицию превращает суперкласс в собственный объект, который становится коллаборатором класса при рефакторинге. Вместо того, чтобы наследовать каждый публичный метод, объект просто предоставит строго необходимые методы.
Этот рефакторинг является одним из самых малоиспользуемых в мире PHP . Не бойтесь опробовать композицию, когда увидите дублированный код.
Почему композиция?
Устранение дублирования через наследование представляет некоторые проблемы.
Во-первых, наследование может использоваться только для повторного использования кода, а не для установления семантических отношений. Абстрактные классы с именами, такими как VehicleAbstract, расширенный от Vehicle, являются искусственными конструкциями, которые ничего не представляют в проблемной области.
Более того, наследование раскрывает все открытые методы суперкласса, возможно, нарушая инкапсуляцию. Это только вопрос времени, когда кто-то вызывает метод, который не должен был быть доступен.
Третья проблема связана с модульным тестированием и дублированием тестового кода . Должны ли мы проверять только поведение подклассов? Или мы должны проверить также унаследованные функции? В последнем случае мы продублируем тестовый код.
Наследование и делегирование (также называемое составом) являются двумя основными отношениями между классами в ООП. Они эквивалентны с теоретической, функциональной точки зрения, но так же, как и машина Тьюринга или язык пробелов .
меры
- Создайте поле в подклассе и инициализируйте его как $ this. Он будет содержать сотрудника.
- Измените методы в подклассе, чтобы использовать поле делегата. Методы, которые наследуются, могут потребоваться для делегирования родителю .
- Удалите объявление подкласса и замените делегат новым экземпляром суперкласса.
На протяжении всего рефакторинга тесты всегда должны проходить. Этот рефакторинг имеет решающее значение, поскольку он открывает дополнительные возможности: например, внедрение зависимостей, выполненное для соавтора, или извлечение интерфейса, содержащего открытые методы, вызываемые прежним подклассом.
пример
Мы начинаем с конца примера метода 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.