Practical PHP Refactoring: Form Template Method
Join the DZone community and get the full member experience.
Join For FreeDuplication is not always expressed as an identical block of code: often it is subtler to discover, because it exists at an higher level of abstraction.
Consider a sorting algorithm, a classic example in computer science: it can be implemented on widely different data structures, as long as their element can be compared and swapped. If we compare quicksort implemented on an array of integers or on a SplLinkedList containing objects, we see the same algorithm.
Form Template Method is about producing common blocks of code by extracting different blocks into methods with the same signature; in our example, the compare and swap operations of a collection-like objects. The common blocks of code can then be pulled up in a subclass.
Why a Template Method?
Eliminating duplication goes beyond targeting the immediately visible cases. The particular case we are tackling with this refactoring is that of methods performing the same higher-level steps, but with some differences in a few lines.
The result of the refactoring is that common code is moved in the superclass, as a Template Method; the implementation of the steps is left to the subclasses as hook methods. They are usually defined as abstract in the superclass, which knows of their existence but not about their implementations. In PHP it's not strictly necessary to define abstract hook methods due to the lack of static checks, but it is desirable since it catches a potential fatal error at loading time instead of at runtime.
Template Method is based on inheritance, so the client code won't have to change. This refactoring is an application of the Open/Closed Principle: it will be easy to add new versions of the algorithm starting from the Template Method, by simply adding a new subclass.
Steps
- Decompose the similar methods: extract only the different small blocks of code into methods with the same signature. Fowler suggests that after this step each pair of analogue methods in the subclasses should be either identical (Template Method) or completely different (hook methods).
- If there are methods different just for the name of the method calls they make, rename the different hook methods they call with the same name and signature.
- Use Extract Superclass if needed, and Pull Up Method to move the identical methods up in it.
- Define signatures of the hook methods as abstract, to ensure they are defined in each subclass.
Example
In the initial state, two classes VideoTweet and ArticleTweet are representing the text for a tweet linking to a video, or an article on DZone.
<?php class FormTemplateMethod extends PHPUnit_Framework_TestCase { public function testAVideoTweet() { $link = new VideoTweet('http://www.youtube.com/watch?...', "Lolcats"); $this->assertEquals('Check out this video: Lolcats http://www.youtube.com/watch?...', $link->__toString()); } public function testAnArticleTweet() { $link = new ArticleTweet('http://css.dzone.com/category/tags/practical-php-refactoring', "Practical PHP Refactoring"); $this->assertEquals('RT @DZone: Practical PHP Refactoring http://css.dzone.com/category/tags/practical-php-refactoring', $link->__toString()); } } class VideoTweet { private $url; private $title; public function __construct($url, $title) { $this->url = $url; $this->title = $title; } public function __toString() { return "Check out this video: $this->title $this->url"; } } class ArticleTweet { private $url; private $title; public function __construct($url, $title) { $this->url = $url; $this->title = $title; } public function __toString() { return "RT @DZone: $this->title $this->url"; } }
A preliminary step is to extract the common parts into a superclass with Extract Superclass, Pull Up Field and Pull Up Constructor Body.
class Tweet { protected $url; protected $title; public function __construct($url, $title) { $this->url = $url; $this->title = $title; } } class VideoTweet extends Tweet { public function __toString() { return "Check out this video: $this->title $this->url"; } } class ArticleTweet extends Tweet { public function __toString() { return "RT @DZone: $this->title $this->url"; } }
Now we begin this refactoring. __toString() will become our Template Method, and we have to extract the different bits into methods with a common signature.
These hook methods are different, while __toString() is now the same for all (two) subclasses:
class VideoTweet extends Tweet { public function __toString() { return $this->prefix() . ": $this->title $this->url"; } protected function prefix() { return "Check out this video"; } } class ArticleTweet extends Tweet { public function __toString() { return $this->prefix() . ": $this->title $this->url"; } protected function prefix() { return "RT @DZone"; } }
We pull up __toString() to eliminate its duplication:
class Tweet { protected $url; protected $title; public function __construct($url, $title) { $this->url = $url; $this->title = $title; } public function __toString() { return $this->prefix() . ": $this->title $this->url"; } } class VideoTweet extends Tweet { protected function prefix() { return "Check out this video"; } } class ArticleTweet extends Tweet { protected function prefix() { return "RT @DZone"; } }
We also define the hook methods as abstract: new classes will have to ensure their presence before they can be instantiated at all. It's also easier to define new type of tweets, since they just take a little prefix() method instead of new copies of __construct() and __toString().
abstract class Tweet { protected $url; protected $title; public function __construct($url, $title) { $this->url = $url; $this->title = $title; } public function __toString() { return $this->prefix() . ": $this->title $this->url"; } /** * @return string */ protected abstract function prefix(); }
Opinions expressed by DZone contributors are their own.
Comments