Practical PHP Refactoring: Replace Delegation with Inheritance
Join the DZone community and get the full member experience.
Join For FreeDelegation is a more flexible solution with respect to inheritance, because it allows to change collaborators by introducing new classes. However, it hides the public protocol of the collaborator into a private field.
If you find yourself writing many delegation methods, you may refactor to inheritance to simplify the code. The subclass will inherit everything automatically, which makes inheritance a powerful weapon.
Why moving towards inheritance?
Fowler cites the growth of delegation code as a force calling for this refactoring. Not all problems can be modelled accurately with delegation, although it is equivalent to inheritance. Depending on the domain and on the entity, you may prefer a Vehicle^-Coach inheritance relationship between classes (where Vehicle contains field like driver) or a Coach-*>Driver composition one.
There are other reasons to choose one over the other, like mutable state in the collaborator. In the inheritance case it cannot be shared between objects, while in the delegation one it can.
A first corollary is that if you want to avoid aliasing and one object changing the state of the other, inheritance will stop this from happening for sure.
A second corollary is that if you want to share date between objects this way instead, you cannot use inheritance.
A warning: all the public methods will be inherited. If you do not use all of them, you're violating LSP by refactoring to inheritance, overusing this construct. Moreover, inheritance is a one-shot strategy: you can use it only once for a class, and afterwards other responsibilities must consider only composition.
Steps
The steps perform the inverse routine of Replace Inheritance With Delegation.
- Make the client object extend the delegate class.
- The delegate should become $this, by initialization in the client constructor.
- Remove delegation: methods should now be inherited; change one method at a time if you're unsure of the effects.
- Remove the delegate field and clean up unnecessary indirections.
Example
We start from the end of the previous example, but it has brought us into a ne wdirection. Most of the state has been pushed into Format, due to the desire to eliminate the duplication of fields into the subclasses (text/content, author). Incidentally, it is not a format anymore, but a name like FeedItem starts to pop up...
__toString() is an example of a method directly delegated to the format object, but there could (and will) be many more in real code.
<?php class ReplaceDelegationWithInheritance extends PHPUnit_Framework_TestCase { public function testAPostShowsItsAuthor() { $post = new Post("Hello, world!", "giorgiosironi"); $this->assertEquals("Hello, world! -- giorgiosironi", $post->__toString()); } public function testALinkShowsItsAuthor() { $link = new Link("http://en.wikipedia.com", "giorgiosironi"); $this->assertEquals("<a href=\"http://en.wikipedia.com\">http://en.wikipedia.com</a> -- giorgiosironi", $link->__toString()); } } class TextSignedByAuthorFormat { private $text; private $author; public function __construct($text, $author) { $this->text = $text; $this->author = $author; } public function __toString() { return "$this->text -- $this->author"; } } class Post { private $format; public function __construct($text, $author) { $this->format = new TextSignedByAuthorFormat($text, $author); } public function __toString() { return $this->format->__toString(); } } class Link { private $format; public function __construct($url, $author) { $this->format = new TextSignedByAuthorFormat($this->displayedText($url), $author); } protected function displayedText($url) { return "<a href=\"$url\">$url</a>"; } public function __toString() { return $this->format->__toString(); } }
While extending the delegate class, we prove it's compatible with the future subclass:
class TextSignedByAuthorFormat { private $text; private $author; public function __construct($text, $author) { $this->text = $text; $this->author = $author; } public function __toString() { return "$this->text -- $this->author"; } } class Post extends TextSignedByAuthorFormat { private $format; public function __construct($text, $author) { $this->format = new TextSignedByAuthorFormat($text, $author); } public function __toString() { return $this->format->__toString(); } } class Link extends TextSignedByAuthorFormat { private $format; public function __construct($url, $author) { $this->format = new TextSignedByAuthorFormat($this->displayedText($url), $author); } protected function displayedText($url) { return "<a href=\"$url\">$url</a>"; } public function __toString() { return $this->format->__toString(); } }
We modify __construct() and remove the delegation in the methods. If we don't, a call will recur indefinitely as $this->method() will call $this->format->method(), which is actually $this->method(), and start again the cycle. Xdebug will discover these problems and block the script at a maximum level of nesting.
class Post extends TextSignedByAuthorFormat { private $format; public function __construct($text, $author) { parent::__construct($text, $author); } } class Link extends TextSignedByAuthorFormat { private $format; public function __construct($url, $author) { parent::__construct($this->displayedText($url), $author); } protected function displayedText($url) { return "<a href=\"$url\">$url</a>"; } }
We can now simplify, by removing the delegate field, renaming the superclass, and deleting constructors which just delegate. Again, delegating methods had to be removed in the previous step unless they substitute $this->format-> with parent::, which delegating methods never use instead.
class FeedItem { private $text; private $author; public function __construct($text, $author) { $this->text = $text; $this->author = $author; } public function __toString() { return "$this->text -- $this->author"; } } class Post extends FeedItem {} class Link extends FeedItem { public function __construct($url, $author) { parent::__construct($this->displayedText($url), $author); } protected function displayedText($url) { return "<a href=\"$url\">$url</a>"; } }
Opinions expressed by DZone contributors are their own.
Comments