Platinum Partner
php,agile,css,practical php refactoring,spl

Practical PHP Refactoring: Introduce Local Extension

Introduce Local Extension is a workaround refactoring used to add logic when you cannot modify the original source code, or a more invasive refactoring is not feasible for the time being. As for Introduce Foreign Method, it works through addition instead of modification of code, so it's a little step towards the Open/Closed Principle.
When you have a class which lacks a few methods you desire to call on its instances, you can introduce an extension class which adds the method you want. This class may be:

  • a subclass, which uses the extends keyword.
  • a wrapper, which keeps the original instance in one of its private fields.

When more and more methods relative to the server object become Foreign Methods, or they started get repeated in different client classes, Introduce Local Extension allows you to keep that same logic in a single class.

Inheritance and composition

It's an old debate, isn't it? In both cases the whole original protocol (set of public methods) should be supported.

By introducing a subclass, you have less work to perform: just add the methods you need and the other public ones will be inherited. This specialization of the refactoring however require intervention in the creation process, or a translating step where the old instance is transformed into a new one; if a framework or a library is creating the original object, it may not be feasible to use a subclass.

Wrapping is an approach which involves more code, since you need to delegate all the methods to the original object. The wrapping class may implement an interface shared with the original object, or rely on duck typing and the use of __call() to quickly delegate every call to not explicitly defined methods.

Wrapping may also involve subclassing to conform to existing type hints; in most of the cases it's not a local extension anymore as you need a different type, for example one that removes methods of the original class for simplification. This kind of wrapping is important in PHP's object-oriented programming paradigm, but it's not related to this refactoring.

Steps

  1. Create an extension class as a subclass or wrapper of the original one.
  2. Add a conversion option to the constructor or a Factory Method when needed. They should take as input an instance of the original class.
  3. Add new methods to the extension class, probably by copying them from existing Foreign Methods.
  4. Replace the usage (usually instantiation) of the original class with the extension where necessary.
  5. Move any remaining foreign methods to the extension class, or delete them if their functionality is already covered.

Example

In the example, we are not satisfied with ArrayObject's Api, and we want to introduce a subclass in order to be able to define more methods. ArrayObject is a sealed class: it's part of the PHP runtime.

<?php
class IntroduceLocalExtension extends PHPUnit_Framework_TestCase
{
    public function testLinksAreViewedInOrder()
    {
        $links = new LinkGroup();
        $links->addUrl('twitter.com');
        $links->add('plus.google.com', 'Google+');
        $links->add('facebook.com', 'Facebook');
        $expected = "<a href=\"facebook.com\">Facebook</a>\n"
                  . "<a href=\"plus.google.com\">Google+</a>\n"
                  . "<a href=\"twitter.com\">twitter.com</a>";
        $this->assertEquals($expected, $links->__toString());
    }
}

class LinkGroup
{
    private $links;

    public function __construct()
    {
        $this->links = new ArrayObject();
    }

    public function add($url, $text)
    {
        $this->newLink($url, $text);
    }

    public function addUrl($url)
    {
        $this->newLink($url, $url);
    }

    public function __toString()
    {
        $links = array();
        foreach ($this->links as $url => $text) {
            $links[] = "<a href=\"$url\">$text</a>";
        }
        return implode("\n", $links);
    }

    /**
     * Foreign Method of the ArrayObject. Should be moved onto a newly extracted
     * collaborator which wraps the ArrayObject, or an heap-like data structure
     * should be used.
     */
    private function newLink($url, $text)
    {
        $this->links[$url] = $text;
        $this->links->asort();
    }
}

We introduce a subclass:

/**
* From PHP 5.3, we can also use SplHeap and derivations.
*/
class TextHeap extends ArrayObject
{
}

And replace the instantiation of the old class:

class LinkGroup
{
    private $links;

    public function __construct()
    {
        $this->links = new TextHeap();
    }

We can now move the method on the new TextHeap class:

class LinkGroup
{
    private $links;

    public function __construct()
    {
        $this->links = new TextHeap();
    }

    public function add($url, $text)
    {
        $this->links->newLink($url, $text);
    }

    public function addUrl($url)
    {
        $this->links->newLink($url, $url);
    }

    public function __toString()
    {
        $links = array();
        foreach ($this->links as $url => $text) {
            $links[] = "<a href=\"$url\">$text</a>";
        }
        return implode("\n", $links);
    }
}

/**
* From PHP 5.3, we can also use SplHeap and derivations.
*/
class TextHeap extends ArrayObject
{
    public function newLink($url, $text)
    {
        $this[$url] = $text;
        $this->asort();
    }

}

And rename some internals to better reflect the fact that we are using an heap and not a simple array:

class LinkGroup
{
    private $heap;

    public function __construct()
    {
        $this->heap = new TextHeap();
    }

    public function add($url, $text)
    {
        $this->heap->addElement($url, $text);
    }

    public function addUrl($url)
    {
        $this->heap->addElement($url, $url);
    }

    public function __toString()
    {
        $links = array();
        foreach ($this->heap as $url => $text) {
            $links[] = "<a href=\"$url\">$text</a>";
        }
        return implode("\n", $links);
    }
}

I've used ArrayObject as an example because it's already a class that can be substituted, but the same refactoring could be performed in the case of a primitive array.

{{ tag }}, {{tag}},

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}
{{ parent.authors[0].realName || parent.author}}

{{ parent.authors[0].tagline || parent.tagline }}

{{ parent.views }} ViewsClicks
Tweet

{{parent.nComments}}