Practical PHP Refactoring: Tease Apart Inheritance
Join the DZone community and get the full member experience.
Join For Freewe are entering into the final part of this series, on large scale refactorings : this kind of operations is less predictable and less immediate. however, it is important to be able to perform them with small steps whenever necessary, if we don't want to get stuck in a situation with dozens of broken classes and no clear further step to take.
sometimes larger investments are needed to avoid a tangled design: these large scale refactorings use the smaller refactorings as building blocks, but they work at an higher level of abstraction and affect many places in your codebase.
why eliminating some inheritance levels?
inheritance is easy to abuse , we got it by now. the main problem is its powerful ability to automatically copy and paste code, which leads to use *extends* any time there is duplication instead of any time there is an inexpressed is-a relationship between concepts..
the result is often that you have to jump up and down a hierarchy to follow the thread of execution, disrupting any separation of concerns between subclass and superclass.
/the case we want to address today is the
combinatorial explosions of concerns
where
each level of subclassing adds a dimension
to the responsibility of the class.
consider for example post and link as subclasses of newsfeeditem. at the second level, we may add facebookpost and twitterpost classes, inheriting from post. but also twitterlink and, maybe tomorrow, facebooklink. if we support linkedin, let's think about linkedinpost... the number of classes grow with the square of the elements involved.
there are several solutions (like the decorator pattern), but our goal is always the same:
keeping a hierarchy as small as possible
by allowing a single categorization. when classes of a unique hierarchy can be put in a matrix, they grow with an order of magnitude more than when a single level of inheritance is present.
fowler explains how a 3d or 4d matrix is even worse, and require this refactoring to be applied more times to each pair of dimensions. if you have ever seen a pair of withimagefacebookpost and textualfacebookpost classes...
steps
- first, identify the different concerns to separate: all the dimensions you can put the classes on. in our continuing example, we are talking about the socialnetwork categorization and the item one.
- choose one of the two dimensions to keep in the current hierarchy, while the other will be extracted.
- perform extract class on the base class of the hierarchy, to introduce a collaborator. this will be the base class of the other hierarchy.
- introduce subclasses for the extracted object , for each of the original ones that have to be eliminated (so we're talking about only the second categorization.). initialize the current objects with them.
- move methods onto to the new hierarchy. you may have to extract them first.
- when subclasses contain only initialization, move the logic in the creation code and eliminate them.
this refactoring is a great enabler for further moves: you can extract other collaborators or methods thanks to the reduced complexity of the hierarchies, that can now diverge. you can also simplify testing, by testing at the unit level and for single concerns.
example
i'll try to keep the logic as small as possible since these examples will use many classes already.
in the initial situation, there is a hierarchy with two levels:
newsfeeditem defines two abstract methods, the content() and the authorlink(), to use for displaying itself. post and link implements content(), while their subclasses implements authorlink() targeting facebook or twitter respectively.
<?php class teaseapartinheritance extends phpunit_framework_testcase { public function testafacebookpostisdisplayedwithtextandlinktotheauthor() { $post = new facebookpost("enjoy!", "php-cola"); $this->assertequals("<p>enjoy!" . " -- <a href=\"http://facebook.com/php-cola\">php-cola</a></p>", $post->__tostring()); } public function testafacebooklinkisdisplayedwithtargetandlinktotheauthor() { $link = new facebooklink("our new ad", "http://youtube.com/...", "php-cola"); $this->assertequals("<p><a href=\"http://youtube.com/...\">our new ad</a>" . " -- <a href=\"http://facebook.com/php-cola\">php-cola</a></p>", $link->__tostring()); } public function testatwitterlinkisdisplayedwithtargetandlinktotheauthor() { $link = new twitterlink("our new ad", "http://youtube.com/...", "giorgiosironi"); $this->assertequals("<p><a href=\"http://youtube.com/...\">our new ad</a>" . " -- <a href=\"http://twitter.com/giorgiosironi\">@giorgiosironi</a></p>", $link->__tostring()); } } abstract class newsfeeditem { protected $author; public function __tostring() { return "<p>" . $this->content() . " -- " . $this->authorlink() . "</p>"; } /** * @return string */ protected abstract function content(); /** * @return string */ protected abstract function authorlink(); } abstract class post extends newsfeeditem { private $content; public function __construct($content, $author) { $this->content = $content; $this->author = $author; } protected function content() { return $this->content; } } abstract class link extends newsfeeditem { private $url; private $linktext; public function __construct($linktext, $url, $author) { $this->linktext = $linktext; $this->url = $url; $this->author = $author; } protected function content() { return "<a href=\"$this->url\">$this->linktext</a>"; } } class facebookpost extends post { protected function authorlink() { return "<a href=\"http://facebook.com/$this->author\">$this->author</a>"; } } class twitterlink extends link { protected function authorlink() { return "<a href=\"http://twitter.com/$this->author\">@$this->author</a>"; } } class facebooklink extends link { protected function authorlink() { return "<a href=\"http://facebook.com/$this->author\">$this->author</a>"; } }
as you can see, the second level introduces some duplicated code that a composition solution will immediately fix. we choose to maintain post and link in the curent hierarchy, since they are also tied to $this->author. the new hierarchy will contain a facebook and a twitter related class.
we add the source new base class for the second hierarchy, and a field in newsfeeditem to hold an instance of it:
abstract class newsfeeditem { protected $author; protected $source; public function __tostring() { return "<p>" . $this->content() . " -- " . $this->authorlink() . "</p>"; } /** * @return string */ protected abstract function content(); /** * @return string */ protected abstract function authorlink(); } abstract class source { public function __construct($author) { $this->author = $author; } public abstract function authorlink(); }
we add the two facebooksource and twittersource subclasses, and we initialize the $source field to the right instance via a init() hook method. a constructor would be equivalent, but right now we would have to delegate to parent::__construct() and that would be noisy.
class facebooksource extends source { public function authorlink() { } } class twittersource extends source { public function authorlink() { } } class facebookpost extends post { public function init() { $this->source = new facebooksource($this->author); } protected function authorlink() { return "<a href=\"http://facebook.com/$this->author\">$this->author</a>"; } } class twitterlink extends link { public function init() { $this->source = new twittersource($this->author); } protected function authorlink() { return "<a href=\"http://twitter.com/$this->author\">@$this->author</a>"; } } class facebooklink extends link { public function init() { $this->source = new facebooksource($this->author); } protected function authorlink() { return "<a href=\"http://facebook.com/$this->author\">$this->author</a>"; } }
we perform move method two times to move the authorlink() behavior in the collaborator. this means we have to delegate to $this->source in the base class newsfeeditem.
abstract class source { public function __construct($author) { $this->author = $author; } public abstract function authorlink(); } class facebooksource extends source { public function authorlink() { return "<a href=\"http://facebook.com/$this->author\">$this->author</a>"; } } class twittersource extends source { public function authorlink() { return "<a href=\"http://twitter.com/$this->author\">@$this->author</a>"; } }
now he second level subclasses only contain creation code: we can move eliminate them if we move this initialization in the construction phase, which is represented by the tests here.
we can substitute $this->author with $this->source:
<?php class teaseapartinheritance extends phpunit_framework_testcase { public function testafacebookpostisdisplayedwithtextandlinktotheauthor() { $post = new facebookpost("enjoy!", new facebooksource("php-cola")); $this->assertequals("<p>enjoy!" . " -- <a href=\"http://facebook.com/php-cola\">php-cola</a></p>", $post->__tostring()); } public function testafacebooklinkisdisplayedwithtargetandlinktotheauthor() { $link = new facebooklink("our new ad", "http://youtube.com/...", new facebooksource("php-cola")); $this->assertequals("<p><a href=\"http://youtube.com/...\">our new ad</a>" . " -- <a href=\"http://facebook.com/php-cola\">php-cola</a></p>", $link->__tostring()); } public function testatwitterlinkisdisplayedwithtargetandlinktotheauthor() { $link = new twitterlink("our new ad", "http://youtube.com/...", new twittersource("giorgiosironi")); $this->assertequals("<p><a href=\"http://youtube.com/...\">our new ad</a>" . " -- <a href=\"http://twitter.com/giorgiosironi\">@giorgiosironi</a></p>", $link->__tostring()); } } abstract class newsfeeditem { protected $author; protected $source; public function __tostring() { return "<p>" . $this->content() . " -- " . $this->source->authorlink() . "</p>"; } /** * @return string */ protected abstract function content(); }
we can now instantiate directly post and link by making them concrete instead of abstract. you may want to bundle this step with the previous one as it intervene on the same code. a consequence is that we can throw away the second level subclasses.
class teaseapartinheritance extends phpunit_framework_testcase { public function testafacebookpostisdisplayedwithtextandlinktotheauthor() { $post = new post("enjoy!", new facebooksource("php-cola")); $this->assertequals("<p>enjoy!" . " -- <a href=\"http://facebook.com/php-cola\">php-cola</a></p>", $post->__tostring()); } public function testafacebooklinkisdisplayedwithtargetandlinktotheauthor() { $link = new link("our new ad", "http://youtube.com/...", new facebooksource("php-cola")); $this->assertequals("<p><a href=\"http://youtube.com/...\">our new ad</a>" . " -- <a href=\"http://facebook.com/php-cola\">php-cola</a></p>", $link->__tostring()); } public function testatwitterlinkisdisplayedwithtargetandlinktotheauthor() { $link = new link("our new ad", "http://youtube.com/...", new twittersource("giorgiosironi")); $this->assertequals("<p><a href=\"http://youtube.com/...\">our new ad</a>" . " -- <a href=\"http://twitter.com/giorgiosironi\">@giorgiosironi</a></p>", $link->__tostring()); } }
the final result can be thought of as a bridge pattern, or just good factoring:
a further step could be to divide these tests into unit ones, only exercising a newsfeeditem or a source object. but that's a story for another day...
Opinions expressed by DZone contributors are their own.
Comments