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: Replace Type Code with Subclasses

09.26.2011
| 5773 views |
  • submit to reddit

This is the second part in the refactoring from type codes miniseries: type codes are scalar fields that can assume a finite number of values.

The assumption of today is that the type code affects the behavior of the class: basing on the value of the field, different code is executed. Typically, the code is selected via an if() or another control structure like select() or ?:. Any code that tests over the value of the type code is suspect.
This time we can't refactor by extracting a single class, because we will just move the ifs into this class, which would have to cover all the different cases.

So we try an inheritance solution: we create different subclasses of the original one instead of extracting a smaller class. Since the behavior depends on a field, at construction time we must know which value resides in the type code field and we can decide which class to instantiate. The original class may become abstract in the process.

Why subclassing?

This refactoring favors polymorphism over control structures: it respects the Single Responsibility Principle by seprating the code enabled by the various type code values.

The next time you add a new value of the type code, you will add a class without touching the already existing ones (Open Closed Principle).

Moreover, you create one subclass for each type code, expressing a concept with a language construct instead of with different values of a string or an integer.

When you cannot apply this refactoring

Fowler cites some of the cases where this refactoring cannot be applied (but don't despair: there are alternatives.)

A simple case is when the type code changes very often. However, I find out that if it changes due to a rare state transition, you can still use the inheritance solution by making the incriminated method return the new instance (e.g. InactiveUser::activate() returns an instance of ActiveUser). This is more difficult to do when the lifecycle of the objects is not only managed by garbage collection but also by external resources like a database accessed via an ORM (you have to make the objects represent the same row/document/blob and check dangling references.)

Another unsuitable scenario occurs when there already is an inheritance hierarchy: inheritance is a solution you can use to manage just one axis of change of a model. In this case, you'll have to pursue a composition solution which we'll see in the next article of this series.

Steps

  1. First of all, self-encapsulation should be executed on the type code, resulting in a getTypeCode() protected method.
  2. For each value of the type code, a subclass should be created, which overrides the getTypeCode() method with an hardcoded one.
  3. Next, we have to tackle creation: if the type code is passed in the constructor, the creation code should be moved in a Factory Method which is able to return an instance of the right subclass. The only if() statements will remain here for now.
  4. At this point, tests should be green. The logic is still tangled into the original class, but the different subclasses are deciding which type code to specify.
  5. We can now remove the type code field; the getTypeCode() can become abstract as all the subclasses will provide one.
  6. Check the test suite again.
Now you can move logic into the subclasses everytime it is specific to a certain value of the type code. In the ideal case, you will also be able to delete the getTypeCode() method when you're finished.

Example

We start from the same User specialization of the previous example; however, this time __toString()'s result depends on the value of the type code.
<?php
class ReplaceTypeCodeWithSubclasses extends PHPUnit_Framework_TestCase
{
    public function testAnUserCanBeANewbie()
    {
        $user = User::newUser("Giorgio", User::NEWBIE);
        $this->assertEquals("Giorgio", $user->__toString());
    }

    public function testAnUserCanBeRegardedAsAGuru()
    {
        $user = User::newUser("Giorgio", User::GURU);
        $this->assertEquals("ADMIN: Giorgio", $user->__toString());
    }
}

class User
{
    const NEWBIE = 'N';
    const GURU = 'G';

    protected $name;

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

    public static function newUser($name, $rank)
    {
        if ($rank == self::GURU) {
            return new Guru($name);
        }
        return new Newbie($name);
    }

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

class Guru extends User
{
    protected function getRank()
    {
        return self::GURU;
    }

    public function __toString()
    {
        return "ADMIN: $this->name";
    }
}

class Newbie extends User
{
    protected function getRank()
    {
        return self::NEWBIE;
    }

    public function __toString()
    {
        return $this->name;
    }
}
First, we self-encapsulate the type code with getRank():
class User
{
    const NEWBIE = 'N';
    const GURU = 'G';

    private $name;
    private $rank;

    public function __construct($name, $rank)
    {
        $this->name = $name;
        $this->rank = $rank;
    }

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

    public function __toString()
    {
        if ($this->getRank() == self::GURU) {
            return "ADMIN: $this->name";
        }
        // self::NEWBIE
        return $this->name;
    }
}

Then we add the two subclasses Newbie and Guru, which override getRank(). We have to change the creation process from a constructor to a Factory Method, which will centralize the ifs into one place.

We also modify the test code accordingly, calling the Factory Method.

<?php
class ReplaceTypeCodeWithSubclasses extends PHPUnit_Framework_TestCase
{
    public function testAnUserCanBeANewbie()
    {
        $user = User::newUser("Giorgio", User::NEWBIE);
        $this->assertEquals("Giorgio", $user->__toString());
    }

    public function testAnUserCanBeRegardedAsAGuru()
    {
        $user = User::newUser("Giorgio", User::GURU);
        $this->assertEquals("ADMIN: Giorgio", $user->__toString());
    }
}

class User
{
    const NEWBIE = 'N';
    const GURU = 'G';

    protected $name;
    private $rank;

    public function __construct($name, $rank)
    {
        $this->name = $name;
        $this->rank = $rank;
    }

    public static function newUser($name, $rank)
    {
        if ($rank == self::GURU) {
            return new Guru($name, null);
        }
        return new Newbie($name, null);
    }

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

    public function __toString()
    {
        if ($this->getRank() == self::GURU) {
            return "ADMIN: $this->name";
        }
        // self::NEWBIE
        return $this->name;
    }
}

class Guru extends User
{
    protected function getRank()
    {
        return self::GURU;
    }
}

class Newbie extends User
{
    protected function getRank()
    {
        return self::NEWBIE;
    }
}

Since the test passes, we can remove the type code and simplify. We move the logic dependent on the old code into the two subclasses.

<?php
class ReplaceTypeCodeWithSubclasses extends PHPUnit_Framework_TestCase
{
    public function testAnUserCanBeANewbie()
    {
        $user = User::newUser("Giorgio", User::NEWBIE);
        $this->assertEquals("Giorgio", $user->__toString());
    }

    public function testAnUserCanBeRegardedAsAGuru()
    {
        $user = User::newUser("Giorgio", User::GURU);
        $this->assertEquals("ADMIN: Giorgio", $user->__toString());
    }
}

class User
{
    const NEWBIE = 'N';
    const GURU = 'G';

    protected $name;

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

    public static function newUser($name, $rank)
    {
        if ($rank == self::GURU) {
            return new Guru($name);
        }
        return new Newbie($name);
    }

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

class Guru extends User
{
    protected function getRank()
    {
        return self::GURU;
    }

    public function __toString()
    {
        return "ADMIN: $this->name";
    }
}

class Newbie extends User
{
    protected function getRank()
    {
        return self::NEWBIE;
    }

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

If the code is not needed for display, we could also remove getRank() once all the subclass-specific logic has been moved down in the hierarchy.

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