Practical PHP Refactoring: Replace Inheritance with Delegation
Join the DZone community and get the full member experience.
Join For FreeWhen a subclass violates the Liskov Substitution Principle, or uses only part of a superclass, it is a warning sign that composition can simplify the design.
Refactoring to composition transform the superclass into an object of its own, which becomes the collaborator of the class under refactoring. Instead of inheriting every public method, the object will just expose the strictly needed methods.
This refactoring is one of the most underused in the PHP world. Don't be afraid to try out composition when you see duplicated code.
Why composition?
The elimination of duplication through inheritance presents some issues.
First, inheritance can be exploited just for code reuse instead of for establishing semantic relationships. Abstract classes with names such as VehicleAbstract, extended by Vehicle, are artificial constructs that do not represent anything in the problem domain.
Moreover, inheritance exposes every public method of the superclass, possibly violating encapsulation. It's only a matter of time before someone calls a method which was not supposed to be available.
The third problem is related to unit testing, and the duplication of test code. Should we test just the subclasses behavior? Or should we test also the inherited features? In the latter case, we will duplicate test code.
Inheritance and delegation (also known as composition) are the two basic relationships between classes in OOP. They are equivalent from a theoretical, functional point of view - but so is a Turing Machine or the whitespace language.
Steps
- Create a field in the subclass, and initialize it to $this. It will contain the collaborator.
- Change the methods in the subclass to use the delegate field. Methods which are inherited may need to be introduced as a delegation to parent.
- Remove the subclass declaration, and replace the delegate with a new instance of the superclass.
Throughout the refactoring, the tests should always pass. This refactoring is crucial as it opens up further possibilities: for example, Dependency Injection performed on the collaborator, or the extraction of an interface containing the public methods called by the former subclass.
Example
We start form the end of the Pull Up Method example: we want to transform the NewsFeedItem superclass into a collaborator with the same behavior.
<?php class ReplaceInheritanceWithDelegation 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()); } } abstract class NewsFeedItem { /** * @var string references the author's Twitter username */ protected $author; /** * @return string an HTML printable version */ public function __toString() { return $this->displayedText() . " -- $this->author"; } /** * @return string */ protected abstract function displayedText(); } class Post extends NewsFeedItem { private $text; public function __construct($text, $author) { $this->text = $text; $this->author = $author; } protected function displayedText() { return $this->text; } } class Link extends NewsFeedItem { private $url; public function __construct($url, $author) { $this->url = $url; $this->author = $author; } protected function displayedText() { return "<a href=\"$this->url\">$this->url</a>"; } }
Public methods cannot be inherited from a collaborator, so as a preliminary step they must be delegated to it.
class Post extends NewsFeedItem { private $text; public function __construct($text, $author) { $this->text = $text; $this->author = $author; } protected function displayedText() { return $this->text; } } class Link extends NewsFeedItem { private $url; public function __construct($url, $author) { $this->url = $url; $this->author = $author; } protected function displayedText() { return "<a href=\"$this->url\">$this->url</a>"; } public function __toString() { return parent::__toString(); } }
Deciding a name for the role of the collaborator is an important step. It is very likely to change with respect to a name that follows LSP and is used for a superclass. We choose Format, since the parent models a way to print out the author and content fields.
We also extract a method, display(), in the superclass, to split the formatting behavior from the wiring to the fields. We plan to use display() as a collaborator, while __toString() was made for inheritance and will be discontinued.
abstract class NewsFeedItem { /** * @var string references the author's Twitter username */ protected $author; /** * @return string an HTML printable version */ public function __toString() { return $this->display($this->displayedText(), $this->author); } public function display($text, $author) { return "$text -- $author"; } /** * @return string */ protected abstract function displayedText(); } class Post extends NewsFeedItem { private $text; private $format; public function __construct($text, $author) { $this->text = $text; $this->author = $author; $this->format = $this; } protected function displayedText() { return $this->text; } public function __toString() { return parent::__toString(); } } class Link extends NewsFeedItem { private $url; private $format; public function __construct($url, $author) { $this->url = $url; $this->author = $author; $this->format = $this; } protected function displayedText() { return "<a href=\"$this->url\">$this->url</a>"; } public function __toString() { return parent::__toString(); } }
We can start using the delegate instead of parent, and of relying on inheritance. __toString() is the only point where we have to intervene:
class Post extends NewsFeedItem { private $text; private $format; public function __construct($text, $author) { $this->text = $text; $this->author = $author; $this->format = $this; } protected function displayedText() { return $this->text; } public function __toString() { return $this->format->display($this->displayedText(), $this->author); } } class Link extends NewsFeedItem { private $url; private $format; public function __construct($url, $author) { $this->url = $url; $this->author = $author; $this->format = $this; } protected function displayedText() { return "<a href=\"$this->url\">$this->url</a>"; } public function __toString() { return $this->format->display($this->displayedText(), $this->author); } }
Now we can eliminate abstract and the abstract method in the superclass, plus the extends keyword in the subclasses. This means now $this->format would be initialized to an instance of TextSignedByAuthorFormat, which is the new name for NewsFeedItem. We also have to push down $this->author.
class TextSignedByAuthorFormat { /** * @return string an HTML printable version */ public function __toString() { return $this->display($this->displayedText(), $this->author); } public function display($text, $author) { return "$text -- $author"; } } class Post { private $text; private $author; private $format; public function __construct($text, $author) { $this->text = $text; $this->author = $author; $this->format = new TextSignedByAuthorFormat(); } protected function displayedText() { return $this->text; } public function __toString() { return $this->format->display($this->displayedText(), $this->author); } } class Link { private $url; private $author; private $format; public function __construct($url, $author) { $this->url = $url; $this->author = $author; $this->format = new TextSignedByAuthorFormat(); } protected function displayedText() { return "<a href=\"$this->url\">$this->url</a>"; } public function __toString() { return $this->format->display($this->displayedText(), $this->author); } }
Finally, we can simplify part of the code. We delete the __toString() on TextSignedByAuthorFormat which is dead code; and inline the displayedMethod() on Post, which served the inheritance-based solution but now is an unnecessary indirection.
class TextSignedByAuthorFormat { public function display($text, $author) { return "$text -- $author"; } } class Post { private $text; private $author; private $format; public function __construct($text, $author) { $this->text = $text; $this->author = $author; $this->format = new TextSignedByAuthorFormat(); } public function __toString() { return $this->format->display($this->text, $this->author); } } class Link { private $url; private $author; private $format; public function __construct($url, $author) { $this->url = $url; $this->author = $author; $this->format = new TextSignedByAuthorFormat(); } protected function displayedText() { return "<a href=\"$this->url\">$this->url</a>"; } public function __toString() { return $this->format->display($this->displayedText(), $this->author); } }
There are many further steps we could make:
- inject the TextSignedByAuthorFormat object. Consequently, if the logic in the collaborator expands we can refactor tests to use a Test Double.
- Move $this->author into the format.
- Apply Extract Interface (Format should be the name), to be able to support multiple output formats. Another implementation could place a link on the author too, or could strip all or some of the tags for displaying in a RSS or in a tweet.
Opinions expressed by DZone contributors are their own.
Comments