Статьи

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

До сих пор мы рассматривали только перемещение элементов объекта (полей и методов и конструктора как специальных методов); мы предположили, что эти члены перемещаются между классами в иерархии, которая уже существует.

Рефакторинг большего масштаба предполагает изменение самой иерархии путем добавления и удаления классов. В сегодняшнем сценарии набор членов (методов или полей) используется только в некоторых экземплярах класса. Предлагаемое решение состоит в извлечении подкласса, куда эти члены могут быть перемещены.

Что представляет собой подкласс

Вы можете думать о классах, в математических терминах, как о точно определенном множестве (обычном, нечетком). Класс представляет собой набор всех своих экземпляров (на самом деле он изоморфен … скажем так).

Подкласс представляет подмножество исходного набора , поэтому только некоторые экземпляры суперкласса будут также экземплярами подкласса.

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

В любом случае, характеристика, которая применяется только к некоторым экземплярам, ​​указывает на невыраженное подмножество , и мы можем извлечь подкласс для его представления.

Более того, все экземпляры, не входящие в подмножество (экземпляры суперкласса, но не подкласса), будут упрощены, поскольку их код не будет охватывать методы и поля, которые не являются необходимыми.

меры

  1. Определите подкласс исходного класса с помощью ключевого слова extends .
  2. Разделите исходный конструктор, если необходимо: инициализация полей, которые находятся только в подклассе, должна быть в конструкторе подкласса, чтобы они не требовали создания экземпляра суперкласса.
  3. Замените вызовы конструктора , которые обычно являются новыми операторами, но также статическими фабричными методами. Здесь помогает отделить бизнес от логики построения, так как все ваши экземпляры будут в объектах создания, таких как Фабрики и Строители.
  4. Now that all client code already refers to the right instances, use Push Down Method and Push Down Field on all the members only interesting for the subclass
  5. The tests should pass.

Since this refactoring involves modyfing the hierarchy and the client code that deals with instantiation, you should execute the whole test suite.

An additional step is suggested by Fowler: there could be some fields that represent information now carried by the hierarchy (e.g. a type code). These data can be the target of Self Encapsulate Field and become fixed into the hierarchy: for example you can substitute a $itemType field with a itemType() method which returns a constant string, and that is redefined in the subclass.

Example

We start from a single class, NewsFeedItem, representing an item in a list of updates shared in a social network.

<?php
class ExtractSubclass extends PHPUnit_Framework_TestCase
{
    public function testAPostShowsItsAuthor()
    {
        $post = new NewsFeedItem("Hello, world!", "giorgiosironi");
        $this->assertEquals("Hello, world! -- @giorgiosironi",
                            $post->__toString());
    }

    public function testALinkShowsItsAuthor()
    {
        $link = new NewsFeedItem("/posts/php-refactoring", "giorgiosironi");
        $this->assertEquals("<a href=\"/posts/php-refactoring\">/posts/php-refactoring</a> -- @giorgiosironi",
                            $link->toHtml());
    }
}

class NewsFeedItem
{
    protected $content;
    protected $author;

    public function __construct($content, $author)
    {
        $this->content = $content;
        $this->author = '@' . ltrim($author, '@');
    }

    /**
     * @return string   an HTML printable version
     */
    public function __toString()
    {
        return "$this->content -- $this->author";
    }

    public function toHtml()
    {
        return "<a href=\"$this->content\">$this->content</a> -- $this->author";
    }
}

We notice there are two patterns of usage, described by the tests:

  • instances representing text.
  • Instances representing a link.

Moreover, the toHtml() method is used only by those instances which model links, as text is enough for ordinary object. We can extract a subclass, Link. Let’s define it:

class Link extends NewsFeedItem
{
}

We replace the instantiations in client code where needed; in this example this code is represented by the tests:

<?php
class ExtractSubclass extends PHPUnit_Framework_TestCase
{
    public function testAPostShowsItsAuthor()
    {
        $post = new NewsFeedItem("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->toHtml());
    }
}

We execute Push Down Method on toHtml(), which is only called for Link objects. There are no other members to move.

class NewsFeedItem
{
    protected $content;
    protected $author;

    public function __construct($content, $author)
    {
        $this->content = $content;
        $this->author = '@' . ltrim($author, '@');
    }

    /**
     * @return string   an HTML printable version
     */
    public function __toString()
    {
        return "$this->content -- $this->author";
    }
}

class Link extends NewsFeedItem
{
    public function toHtml()
    {
        return "<a href=\"$this->content\">$this->content</a> -- $this->author";
    }
}

An additional step could be to redefine the constructor to provide a more precise API, calling the first field $url or $path instead of $content.