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 635 posts at DZone. You can read more from them at their website. View Full User Profile

Practical PHP Refactoring: Move Field

07.18.2011
| 4537 views |
  • submit to reddit

Object-oriented programming is based on the encapsulation of state and behavior associated with that state in decoupled items called objects. In the previous issue of this series, we saw how to move behavior to match existing state, while today we'll see the inverse: how to move state representations between classes to better match the methods defined on them. This adjustment of state variables is composed by many Move Field refactorings.

Why should I Move a Field?

Fields should follow the methods referencing them: there are some reason to perform this kind of refactoring which are identical to the ones for Move Method. For example, you may avoid getters and setters since the code working on the fields gets closer to them. Moreover, in the case of classes extraction, moving fields is necessary since the new class is initially empty: this refactoring become a step in a larger process.

There are also a few technological reasons to influence you: for example, you may need to keep the field out or in the session. It happened to me that a PDO was reached by a variable stored in $_SESSION, and thus causing great explosion (PDO connections cannot be serialized.) In these scenarios, changing the location of the problematic field is the simplest step.

A more philosophical question is why this field is in the wrong place? Because of new design decisions that update the old picture. For example, the multiplicity of a field may change (like in our code sample); in any case, fields and methodsget in the wrong place all the time without actually moving. The important part is to acknowledge that a bit of refactoring is needed.

Steps

These steps let you move a field from a source class to a target one.

Step 0 is check that the field is private: in case it's not, encapsulation is required before starting.

  1. Create a field in the target class, accessible for now with a getter and a setter.
  2. Temporarily reference the target object from the source class.
  3. Replace references to the old field with references to the new field. This should comprehend also initialization, of course.
  4. Remove the old field on the source class, since it is now unused.

Throughout all these steps, you can run tests at the functional level, being these an inter-class refactoring. Unit tests for the two classes involved should be modified accordingly: the initialization of the field should move to the target class tests; if encapsulation is working well, tests shouldn't shift from one class to the other, otherwise you're moving a method, not just a field.

This refactoring is perfectly suited to your case when the final version of the source class has just a setter/getter contract over the field with the target one. In this case, the field has been totalle encapsulated and the code can furtherly be simplified by eliminating the reference as we'll see in the example.

Example

We continue with the TagCloud and Link classes example. A new requirement which has been satisfied is to place an attribute on links: rel="archives". We imagine that originally this requirement was defined on a per-link basis, but now we simplify the design by considering only one value for rel in the whole tag cloud.

In the initial state, there is a getter which exposes the field.

<?php
class MoveMethodTest extends PHPUnit_Framework_TestCase
{
    public function testDisplayItsLinksInShortForm()
    {
        $tagCloud = new TagCloud(array(
            new Link('http://giorgiosironi.blogspot.com/search/label/productivity', 'archives'),
            new Link('http://giorgiosironi.blogspot.com/search/label/software%20development', 'archives')
        ));
        $html = $tagCloud->toHtml();
        $this->assertEquals(
            "<a href=\"http://giorgiosironi.blogspot.com/search/label/productivity\" rel=\"archives\">productivity</a>\n"
          . "<a href=\"http://giorgiosironi.blogspot.com/search/label/software%20development\" rel=\"archives\">software development</a>\n",
            $html
        );
    }
}

class TagCloud
{
    private $links;

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

    public function toHtml()
    {
        $html = '';
        foreach ($this->links as $link) {
            $text = $link->text();
            $rel = $link->getRel();
            $html .= "<a href=\"$link\" rel=\"$rel\">$text</a>\n";
        }
        return $html;
    }
}

class Link
{
    private $url;
    private $rel;

    public function __construct($url, $rel = null)
    {
        $this->url = $url;
        $this->rel = $rel;
    }

    public function __toString()
    {
        return $this->url;
    }

    public function getRel()
    {
        return $this->rel;
    }

    public function text()
    {
        $lastFragment = substr(strrchr($this, '/'), 1);
        return str_replace('%20', ' ', $lastFragment);
    }
}

But TagCloud is generating the HTML, while Link is a conceptual link, not an anchor. We introduce two parallel designs: the field continues to exist temporarily. Note that this time we move the field instead of moving the behavior to follow the field, since the field is related to HTML generation, and thus better suited to TagCloud's current responsibility.

<?php
class MoveMethodTest extends PHPUnit_Framework_TestCase
{
    public function testDisplayItsLinksInShortForm()
    {
        $tagCloud = new TagCloud(array(
            new Link('http://giorgiosironi.blogspot.com/search/label/productivity', 'archives'),
            new Link('http://giorgiosironi.blogspot.com/search/label/software%20development', 'archives')
        ), 'archives');
        $html = $tagCloud->toHtml();
        $this->assertEquals(
            "<a href=\"http://giorgiosironi.blogspot.com/search/label/productivity\" rel=\"archives\">productivity</a>\n"
          . "<a href=\"http://giorgiosironi.blogspot.com/search/label/software%20development\" rel=\"archives\">software development</a>\n",
            $html
        );
    }
}

class TagCloud
{
    private $links;
    private $rel;

    public function __construct(array $links, $rel)
    {
        $this->links = $links;
        $this->rel = $rel;
    }

    public function toHtml()
    {
        $html = '';
        foreach ($this->links as $link) {
            $text = $link->text();
            $rel = $link->getRel();
            $html .= "<a href=\"$link\" rel=\"$rel\">$text</a>\n";
        }
        return $html;
    }
}

We can now switch to the usage of the new field. If the source class needed to access the field, the switch would have to provide a reference to TagCloud to the Link. In this particular case, I probably would write a Factory Method on TagCloud to address the construction of Link and avoid exposing this complication.

    public function toHtml()
    {
        $html = '';
        foreach ($this->links as $link) {
            $text = $link->text();
            $html .= "<a href=\"$link\" rel=\"$this->rel\">$text</a>\n";
        }
        return $html;
    }

To get to the final state, we can now delete the field from Link, along with its getter. We do not have to initialize pass it anymore to the Link constructor in the tests.

class Link
{
    private $url;

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

    public function __toString()
    {
        return $this->url;
    }

    public function text()
    {
        $lastFragment = substr(strrchr($this, '/'), 1);
        return str_replace('%20', ' ', $lastFragment);
    }
}
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.)