Когда подкласс нарушает принцип подстановки Лискова или использует только часть суперкласса, это предупреждающий знак того, что композиция может упростить конструкцию.
Рефакторинг в композицию превращает суперкласс в собственный объект, который становится коллаборатором класса при рефакторинге. Вместо того, чтобы наследовать каждый публичный метод, объект просто предоставит строго необходимые методы.
Этот рефакторинг является одним из самых малоиспользуемых в мире 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.