Practical PHP Refactoring: Replace Type Code with Subclasses
Join the DZone community and get the full member experience.
Join For FreeThis 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
- First of all, self-encapsulation should be executed on the type code, resulting in a getTypeCode() protected method.
- For each value of the type code, a subclass should be created, which overrides the getTypeCode() method with an hardcoded one.
- 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.
- 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.
- We can now remove the type code field; the getTypeCode() can become abstract as all the subclasses will provide one.
- Check the test suite again.
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.
Opinions expressed by DZone contributors are their own.
Comments