Мы находимся в той части серии, где рефакторинг направлен главным образом на устранение дублированного кода. На данный момент большинство решений достигнет этой цели с помощью наследования.
Pull Up Method рефакторинга определили дублированный метод , которые находятся в нескольких подклассов и факторов его в существующем общего базового класса; Извлечение общего класса — это работа других рефакторингов.
Как и в случае с рефакторингом Pull Up Field, копии метода могут не совпадать на первый взгляд. Мы можем принять это определение: два метода дублируются, если они дают одинаковые результаты для всех тестов (которые должны охватывать одни и те же тестовые случаи для всех задействованных подклассов).
Имея это в виду, рефакторинг будет остановлен тестами в случае, если методы действительно различны для некоторого углового случая. Если вы можете выполнить предварительный рефакторинг, который делает копии метода фактически идентичными, сделайте это и затем вернитесь сюда: будет проще продолжить.
Зачем?
Наследование всегда было одним из самых простых способов поделиться общим кодом. Всякий раз, когда метод вызывается, клиентский код не должен изменяться . Иногда это на самом деле слишком просто: мы рассмотрим делегирование вместо наследования в следующих эпизодах.
Исключение дублирования на основе наследования может стать почти рефлексом, особенно если мы просто назовем класс AbstractXxxxx и подтянем все. Делегирование требует больше работы, но дает лучшие результаты, так как открываются новые концепции и методы эффективно скрываются за новым объектом, а не остаются в том же API.
Таким образом, наследование является одним из наших средств (наряду с удивлением, страхом и безжалостной эффективностью ) для устранения дублированного кода в исходных файлах; но иногда это просто копирование и вставка с помощью интерпретатора: все подклассы все еще имеют дублированный метод при создании экземпляра объекта, но по крайней мере он генерируется из единственной копии в исходном коде. Во многих случаях вам придется тестировать метод как минимум дважды.
меры
- Убедитесь, что методы на самом деле идентичны . Убедитесь, что подписи также одинаковы: количество параметров, их значение и их имена должны быть одинаковыми во всех задействованных подклассах.
- Создайте новый метод в общем суперклассе и скопируйте в него тело метода.
- Удалите один метод подкласса за раз , запуская тесты после каждого.
Фаулер отмечает, что иногда одним из обязательных типов методов, принимающих экземпляры подклассов, может стать суперкласс. На самом деле это одна из немногих проверок типов, которые мы можем выполнить в PHP, подсказки типа:
public function clientCodeMethod(MySubClassObject $object)
может стать (не обязательно)
public function clientCodeMethod(MyBaseClassObject $object)
в случае clientCodeMethod () вызывает только подтянутые методы.
пример
Мы перезапускаем с конца
примера Pull Up Field : мы унифицировали определение поля $ author, но между двумя подклассами все еще есть дублированный код. На этот раз мы попытаемся исключить несколько версий __toString (), которые очень похожи.
<?php class PullUpMethod 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; } class Post extends NewsFeedItem { private $text; public function __construct($text, $author) { $this->text = $text; $this->author = $author; } public function __toString() { return "$this->text -- $this->author"; } } class Link extends NewsFeedItem { private $url; public function __construct($url, $author) { $this->url = $url; $this->author = $author; } public function __toString() { return "<a href=\"$this->url\">$this->url</a> -- $this->author"; } }
Прежде всего, мы должны убедиться, что два метода объединения одинаковы. Мы должны извлечь метод, инкапсулирующий их различия, и преобразовать __toString () в простой метод Template.
class Post extends NewsFeedItem { private $text; public function __construct($text, $author) { $this->text = $text; $this->author = $author; } private function displayedText() { return $this->text; } public function __toString() { return $this->displayedText() . " -- $this->author"; } } class Link extends NewsFeedItem { private $url; public function __construct($url, $author) { $this->url = $url; $this->author = $author; } private function displayedText() { return "<a href=\"$this->url\">$this->url</a>"; } public function __toString() { return $this->displayedText() . " -- $this->author"; } }
Теперь можно вызвать __toString (): давайте создадим его в суперклассе и пока оставим его в тени. Как и в случае с Pull Up Field, мы можем предоставить здесь документацию по методу. Поскольку __toString () использует метод ловушки, мы также определяем его здесь как абстрактный, хотя он не имеет ничего общего с этим рефакторингом. Реализации displayText () также должны стать защищенными.
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; } public function __toString() { return $this->displayedText() . " -- $this->author"; } } 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 $this->displayedText() . " -- $this->author"; } }
Мы исключаем первое переопределение из Post:
class Post extends NewsFeedItem { private $text; public function __construct($text, $author) { $this->text = $text; $this->author = $author; } protected function displayedText() { return $this->text; } }
И второй из Link:
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>"; } }
Два класса не содержат (почти) повторяющийся код. Мы могли бы продолжить и сделать то же самое с конструктором, но, поскольку он просто присваивает $ author поле, вероятно, не стоит извлекать и извлекать операцию.