DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
  1. DZone
  2. Coding
  3. Languages
  4. Practical PHP Refactoring: Replace Type Code with Subclasses

Practical PHP Refactoring: Replace Type Code with Subclasses

Giorgio Sironi user avatar by
Giorgio Sironi
·
Sep. 26, 11 · Interview
Like (0)
Save
Tweet
Share
8.02K Views

Join the DZone community and get the full member experience.

Join For Free

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.

PHP

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • 7 Most Sought-After Front-End Frameworks for Web Developers
  • Using GPT-3 in Our Applications
  • Build an Automated Testing Pipeline With GitLab CI/CD and Selenium Grid
  • Practical Example of Using CSS Layer

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: