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: Push Down Field

01.09.2012
| 3515 views |
  • submit to reddit

As happens often for a method, a field may be present in the base class of a hierarchy even if it is actually used only in some of the subclasses. This scenarion can be the result of the evolution of the code from some previous state where that field was needed in the base class.

The solution proposed by the Push Down Field refactoring is to push down the field to the subclasses that actually need it, if necessary iteratively.

Why pushing down instead of up?

This refactoring is the opposite of Pull Up Field: a field is transported to a few subclasses instead of a few fields being unified in a single one.

There is a more general theme represented by Push Down Method and Push Down Field: every refactoring is bidirectional and you can go into one or the other direction depending on your situation. Usually, one direction goes towards eliminating duplication while the other temporarily creates it before the duplicated elements become different from each other (you could duplicate a field to make the copies evolve independently). For larger-scale refactorings, one direction increases the number of elements in the design, while the other reduces them to simplify the big picture.

For what regards Push Down Field, it's easy to verify feasibility: fields have usually a very limited scope, which can be private or protected (and never public in OOP):

  • if the field is private, it can't be moved from the current class without also targeting the methods that reference it.
  • If the field is protected, you know you only have to check the code in the current hierarchy to see where it has to be moved and where it would be ignored.

Steps

The steps are very similar to the ones for pushing down a method *link*:

  1. declare the field in all subclasses.
  2. Remove the field from the superclass.
  3. Check that the tests green.
  4. Remove the field from the subclasses, one at a time.
  5. Check again that the tests are green.

Test won't always save you due to how PHP fields work: they will fail only from errors like redefining a field in a subclass with a stricter visibility. If you leave out the definition of a field where it is actually needed, a public field will be created at its first modification.

In this case, an error may or may not show up in tests: if someone tries to access the field he will find null and this may cause an error that the test captures. On the other hand, if the field has NULL as default value, it is impossible to distinguish between a specified one and a version created on-the-fly, if not for the scope.

You could write a test that check that a field is private, but it's a test likely to violate encapsulation and not a really good idea.

Example

In the initial state, we see two Post and Link subclasses. $url is only needed in Link objects, however, and we want to move it down to this class only.

<?php
class PushDownField 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("/posts/php-refactoring", "giorgiosironi");
        $this->assertEquals("<a href=\"/posts/php-refactoring\">/posts/php-refactoring</a> -- @giorgiosironi",
                            $link->__toString());
    }
}

abstract class NewsFeedItem
{
    /**
     * @var string  references the author's Twitter username
     */
    protected $author;

    /**
     * @var string
     */
    protected $url;

    public function __construct($author)
    {
        $this->author = '@' . ltrim($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)
    {
        parent::__construct($author);
        $this->text = $text;
    }

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

class Link extends NewsFeedItem
{
    public function __construct($url, $author)
    {
        parent::__construct($author);
        $this->url = $url;
    }

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

We copy $url into the various subclasses, not modifying the external interface of these objects (since the superclass is abstract, any object in the hierarchy is an instance of Post or Link.)

class Post extends NewsFeedItem
{
    private $text;

    /**
     * @var string
     */
    protected $url;

    public function __construct($text, $author)
    {
        parent::__construct($author);
        $this->text = $text;
    }

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

class Link extends NewsFeedItem
{
    /**
     * @var string
     */
    protected $url;

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

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

We delete the field from the superclass.

abstract class NewsFeedItem
{
    /**
     * @var string  references the author's Twitter username
     */
    protected $author;

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

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

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

We delete the field also from Post.

class Post extends NewsFeedItem
{
    private $text;

    public function __construct($text, $author)
    {
        parent::__construct($author);
        $this->text = $text;
    }

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

Deleting the field from Link won't cause errors, but will make the field public. For documentation and encapsulation purposes it's best to leave this copy in place, as we can clearly see it will be accessed by the code in the Link class.

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.)