Agile Zone is brought to you in partnership with:

I am a programmer and architect (the kind that writes code) with a focus on testing and open source; I maintain the PHPUnit_Selenium project. I believe programming is one of the hardest and most beautiful jobs in the world. Giorgio is a DZone MVB and is not an employee of DZone and has posted 638 posts at DZone. You can read more from them at their website. View Full User Profile

Practical PHP Refactoring: Tease Apart Inheritance

02.06.2012
| 4226 views |
  • submit to reddit

We are entering into the final part of this series, on large scale refactorings: this kind of operations is less predictable and less immediate. However, it is important to be able to perform them with small steps whenever necessary, if we don't want to get stuck in a situation with dozens of broken classes and no clear further step to take.

Sometimes larger investments are needed to avoid a tangled design: these large scale refactorings use the smaller refactorings as building blocks, but they work at an higher level of abstraction and affect many places in your codebase.

Why eliminating some inheritance levels?

Inheritance is easy to abuse, we got it by now. The main problem is its powerful ability to automatically copy and paste code, which leads to use *extends* any time there is duplication instead of any time there is an inexpressed is-a relationship between concepts..

The result is often that you have to jump up and down a hierarchy to follow the thread of execution, disrupting any separation of concerns between subclass and superclass.

/The case we want to address today is the combinatorial explosions of concerns where each level of subclassing adds a dimension to the responsibility of the class.
Consider for example Post and Link as subclasses of NewsFeedItem. At the second level, we may add FacebookPost and TwitterPost classes, inheriting from Post. But also TwitterLink and, maybe tomorrow, FacebookLink. If we support LinkedIn, let's think about LinkedInPost... The number of classes grow with the square of the elements involved.

There are several solutions (like the Decorator pattern), but our goal is always the same: keeping a hierarchy as small as possible by allowing a single categorization. When classes of a unique hierarchy can be put in a matrix, they grow with an order of magnitude more than when a single level of inheritance is present.
Fowler explains how a 3D or 4D matrix is even worse, and require this refactoring to be applied more times to each pair of dimensions. If you have ever seen a pair of WithImageFacebookPost and TextualFacebookPost classes...

Steps

  1. First, identify the different concerns to separate: all the dimensions you can put the classes on. In our continuing example, we are talking about the SocialNetwork categorization and the Item one.
  2. Choose one of the two dimensions to keep in the current hierarchy, while the other will be extracted.
  3. Perform Extract Class on the base class of the hierarchy, to introduce a collaborator. This will be the base class of the other hierarchy.
  4. Introduce subclasses for the extracted object, for each of the original ones that have to be eliminated (so we're talking about only the second categorization.). Initialize the current objects with them.
  5. Move methods onto to the new hierarchy. You may have to extract them first.
  6. When subclasses contain only initialization, move the logic in the creation code and eliminate them.

This refactoring is a great enabler for further moves: you can extract other collaborators or methods thanks to the reduced complexity of the hierarchies, that can now diverge. You can also simplify testing, by testing at the unit level and for single concerns.

Example

I'll try to keep the logic as small as possible since these examples will use many classes already.

In the initial situation, there is a hierarchy with two levels:

 

 


NewsFeedItem defines two abstract methods, the content() and the authorLink(), to use for displaying itself. Post and Link implements content(), while their subclasses implements authorLink() targeting Facebook or Twitter respectively.

<?php
class TeaseApartInheritance extends PHPUnit_Framework_TestCase
{
    public function testAFacebookPostIsDisplayedWithTextAndLinkToTheAuthor()
    {
        $post = new FacebookPost("Enjoy!", "PHP-Cola");
        $this->assertEquals("<p>Enjoy!"
                          . " -- <a href=\"http://facebook.com/PHP-Cola\">PHP-Cola</a></p>",
                            $post->__toString());
    }

    public function testAFacebookLinkIsDisplayedWithTargetAndLinkToTheAuthor()
    {
        $link = new FacebookLink("Our new ad", "http://youtube.com/...", "PHP-Cola");
        $this->assertEquals("<p><a href=\"http://youtube.com/...\">Our new ad</a>"
                          . " -- <a href=\"http://facebook.com/PHP-Cola\">PHP-Cola</a></p>",
                            $link->__toString());
    }

    public function testATwitterLinkIsDisplayedWithTargetAndLinkToTheAuthor()
    {
        $link = new TwitterLink("Our new ad", "http://youtube.com/...", "giorgiosironi");
        $this->assertEquals("<p><a href=\"http://youtube.com/...\">Our new ad</a>"
                          . " -- <a href=\"http://twitter.com/giorgiosironi\">@giorgiosironi</a></p>",
                            $link->__toString());
    }
}

abstract class NewsFeedItem
{
    protected $author;

    public function __toString()
    {
        return "<p>"
             . $this->content()
             . " -- "
             . $this->authorLink()
             . "</p>";
    }

    /**
     * @return string
     */
    protected abstract function content();

    /**
     * @return string
     */
    protected abstract function authorLink();
}

abstract class Post extends NewsFeedItem
{
    private $content;

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

    protected function content()
    {
        return $this->content;
    }
}

abstract class Link extends NewsFeedItem
{
    private $url;
    private $linkText;

    public function __construct($linkText, $url, $author)
    {
        $this->linkText = $linkText;
        $this->url = $url;
        $this->author = $author;
    }

    protected function content()
    {
        return "<a href=\"$this->url\">$this->linkText</a>";
    }
}

class FacebookPost extends Post
{
    protected function authorLink()
    {
        return "<a href=\"http://facebook.com/$this->author\">$this->author</a>";
    }
}

class TwitterLink extends Link
{
    protected function authorLink()
    {
        return "<a href=\"http://twitter.com/$this->author\">@$this->author</a>";
    }
}

class FacebookLink extends Link
{
    protected function authorLink()
    {
        return "<a href=\"http://facebook.com/$this->author\">$this->author</a>";
    }
}

As you can see, the second level introduces some duplicated code that a composition solution will immediately fix. We choose to maintain Post and Link in the curent hierarchy, since they are also tied to $this->author. The new hierarchy will contain a Facebook and a Twitter related class.

We add the Source new base class for the second hierarchy, and a field in NewsFeedItem to hold an instance of it:

abstract class NewsFeedItem
{
    protected $author;
    protected $source;

    public function __toString()
    {
        return "<p>"
             . $this->content()
             . " -- "
             . $this->authorLink()
             . "</p>";
    }

    /**
     * @return string
     */
    protected abstract function content();

    /**
     * @return string
     */
    protected abstract function authorLink();
}

abstract class Source
{
    public function __construct($author)
    {
        $this->author = $author;
    }

    public abstract function authorLink();
}

We add the two FacebookSource and TwitterSource subclasses, and we initialize the $source field to the right instance via a init() hook method. A constructor would be equivalent, but right now we would have to delegate to parent::__construct() and that would be noisy.

class FacebookSource extends Source
{
    public function authorLink()
    {
    }
}

class TwitterSource extends Source
{
    public function authorLink()
    {
    }
}

class FacebookPost extends Post
{
    public function init() { $this->source = new FacebookSource($this->author); }

    protected function authorLink()
    {
        return "<a href=\"http://facebook.com/$this->author\">$this->author</a>";
    }
}

class TwitterLink extends Link
{
    public function init() { $this->source = new TwitterSource($this->author); }

    protected function authorLink()
    {
        return "<a href=\"http://twitter.com/$this->author\">@$this->author</a>";
    }
}

class FacebookLink extends Link
{
    public function init() { $this->source = new FacebookSource($this->author); }

    protected function authorLink()
    {
        return "<a href=\"http://facebook.com/$this->author\">$this->author</a>";
    }
}

We perform Move Method two times to move the authorLink() behavior in the collaborator. This means we have to delegate to $this->source in the base class NewsFeedItem.

abstract class Source
{
    public function __construct($author)
    {
        $this->author = $author;
    }

    public abstract function authorLink();
}

class FacebookSource extends Source
{
    public function authorLink()
    {
        return "<a href=\"http://facebook.com/$this->author\">$this->author</a>";
    }
}

class TwitterSource extends Source
{
    public function authorLink()
    {
        return "<a href=\"http://twitter.com/$this->author\">@$this->author</a>";
    }
}

Now he second level subclasses only contain creation code: we can move eliminate them if we move this initialization in the construction phase, which is represented by the tests here.

We can substitute $this->author with $this->source:

<?php
class TeaseApartInheritance extends PHPUnit_Framework_TestCase
{
    public function testAFacebookPostIsDisplayedWithTextAndLinkToTheAuthor()
    {
        $post = new FacebookPost("Enjoy!", new FacebookSource("PHP-Cola"));
        $this->assertEquals("<p>Enjoy!"
                          . " -- <a href=\"http://facebook.com/PHP-Cola\">PHP-Cola</a></p>",
                            $post->__toString());
    }

    public function testAFacebookLinkIsDisplayedWithTargetAndLinkToTheAuthor()
    {
        $link = new FacebookLink("Our new ad", "http://youtube.com/...", new FacebookSource("PHP-Cola"));
        $this->assertEquals("<p><a href=\"http://youtube.com/...\">Our new ad</a>"
                          . " -- <a href=\"http://facebook.com/PHP-Cola\">PHP-Cola</a></p>",
                            $link->__toString());
    }

    public function testATwitterLinkIsDisplayedWithTargetAndLinkToTheAuthor()
    {
        $link = new TwitterLink("Our new ad", "http://youtube.com/...", new TwitterSource("giorgiosironi"));
        $this->assertEquals("<p><a href=\"http://youtube.com/...\">Our new ad</a>"
                          . " -- <a href=\"http://twitter.com/giorgiosironi\">@giorgiosironi</a></p>",
                            $link->__toString());
    }
}

abstract class NewsFeedItem
{
    protected $author;
    protected $source;

    public function __toString()
    {
        return "<p>"
             . $this->content()
             . " -- "
             . $this->source->authorLink()
             . "</p>";
    }

    /**
     * @return string
     */
    protected abstract function content();
}

We can now instantiate directly Post and Link by making them concrete instead of abstract. You may want to bundle this step with the previous one as it intervene on the same code. A consequence is that we can throw away the second level subclasses.

class TeaseApartInheritance extends PHPUnit_Framework_TestCase
{
    public function testAFacebookPostIsDisplayedWithTextAndLinkToTheAuthor()
    {
        $post = new Post("Enjoy!", new FacebookSource("PHP-Cola"));
        $this->assertEquals("<p>Enjoy!"
                          . " -- <a href=\"http://facebook.com/PHP-Cola\">PHP-Cola</a></p>",
                            $post->__toString());
    }

    public function testAFacebookLinkIsDisplayedWithTargetAndLinkToTheAuthor()
    {
        $link = new Link("Our new ad", "http://youtube.com/...", new FacebookSource("PHP-Cola"));
        $this->assertEquals("<p><a href=\"http://youtube.com/...\">Our new ad</a>"
                          . " -- <a href=\"http://facebook.com/PHP-Cola\">PHP-Cola</a></p>",
                            $link->__toString());
    }

    public function testATwitterLinkIsDisplayedWithTargetAndLinkToTheAuthor()
    {
        $link = new Link("Our new ad", "http://youtube.com/...", new TwitterSource("giorgiosironi"));
        $this->assertEquals("<p><a href=\"http://youtube.com/...\">Our new ad</a>"
                          . " -- <a href=\"http://twitter.com/giorgiosironi\">@giorgiosironi</a></p>",
                            $link->__toString());
    }
}

The final result can be thought of as a Bridge pattern, or just good factoring:

A further step could be to divide these tests into unit ones, only exercising a NewsFeedItem or a Source object. But that's a story for another day...

Published at DZone with permission of Giorgio Sironi, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)