With the generalization methods we want to introduce order and clarity in class hierarchies. There are methods that deal with moving class features in hierarchy to the subclass or to the super class (pull up/down attributes, pull up/down method), there is a method that takes care of standardizing the constructor and extracts it on the super class (pull up constructor body), and another very interesting method that helps us to implement the template method pattern of “Gang of Four”[GOF], which is useful when we have methods that perform the same steps but the steps are different.
We will see how to extract subclasses or super classes, useful when the single class tends to encapsulate different behaviors in different instances, or to help us understand how to collapse a hierarchy when useless.
Using inheritance in object-oriented programming is a good habit, but sometimes it can create confusion—for example, when some super class methods or parameters do not represent their subclasses, and it would be better to implement a delegation,. In this chapter we will also see how we can replace inheritance with delegation or replace the delegation with inheritance.
The collection of refactoring techniques that we'll present in this chapter was created by Martin Fowler [FOW01]. For each method, we'll see the motivation and situation for using it, the mechanism that will explain how we can apply the method to our existing code, and some examples of how each method works in a real-world case.
Problem: “Two subclasses have the same field.”
Solution: “Move the field to the super class.”
When we find two classes that extend the same super class having the same attributes, we can move these duplicated attributes in the super class.
It may happen that the same attributes have different names. In this case, check that the attributes are equal, and that they are used in the same way. If we are sure, we can rename the attribute in one of two classes, move it to the super class and remove them from their subclasses.
After some refactoring steps, we have two classes—User
and Admin
—that extend the BaseUser
class. These two classes were initially independent, and therefore both have the same attribute $username
.
class User extends BaseUser
{
public $username;
public function getUsernameWithLabel()
{
return 'User: '.$this->username;
}
}
class Admin extends BaseUser
{
public $username;
public function getUsernameWithLabel()
{
return 'Admin: '.$this->username;
}
}
Before moving the common attribute in the super class, we write a unit test that preserves the proper class behavior.
class UserTest extends PHPUnit_Framework_TestCase
{
public function testGetUsername()
{
$admin = new Admin();
$admin->username = 'cphp';
$this->assertEquals('Admin: cphp', $admin->getUsernameWithLabel());
$user = new User();
$user->username = 'cphp';
$this->assertEquals('User: cphp', $user->getUsernameWithLabel());
}
}
Run the test and verify that everything works.
PHPUnit 3.4.1 by Sebastian Bergmann.
.
Time: 1 second
OK (1 test, 2 assertions)
At this point, following the mechanism, we identify similar class attributes. In our case we have the $username
attribute that is duplicated in both classes. After checking that it's used in the same way both in the User
class and in the Admin
class, we can create a new attribute with the same name in a super class and remove it from the subclasses.
abstract class BaseUser
{
public $username;
}
class User extends BaseUser
{
public function getUsernameWithLabel()
{
return 'User: '.$this->username;
}
}
class Admin extends BaseUser
{
public function getUsernameWithLabel()
{
return 'Admin: '.$this->username;
}
}
Run the unit test again and verify that the behavior of the classes has not changed.
Problem: “You have methods with identical results on subclasses.”
Solution: “Move them to the super class.”
As we have said several times, duplicate code is one of the most dangerous bad smells, because where there is duplicate code, bugs can proliferate. Every time we duplicate even a single line of code, we should ask what would happen if we apply a change in one of the two lines, forgetting the other.
Through the copy and paste action often we can find the exact same methods duplicated in subclasses that extend the same super class. In this case we can move the duplicated method in the super class and delete it from all subclasses.
In other cases we have methods with the same body but with different interfaces. In these cases we must standardize the interface, move the method in the super class, and remove the original methods from the subclasses.
Another case is when we have a method of a subclass, which overrides a super class method, and the body remains unchanged, producing the same behavior.
If we move the method body into the super class, but we have other methods and attributes that aren't in the super class, we can implement some strategies If we're using attributes present only in a subclass, we can move these attributes to a super class. If we're using methods present only in a subclass we can move these methods to a super class if all subclasses use it, or create an abstract method on a super class.
If we have similar methods that are not exactly equal, we can apply the “Form Template” technique, which we'll see after in this chapter.
Consider an abstract super class BaseUser
with two subclasses, Admin
and User
.
abstract class BaseUser
{
protected $credentials;
public function hasCredential($credential)
{
return in_array($credential, $this->credentials);
}
}
class Admin extends BaseUser
{
public function initCredentials($credentials)
{
$this->credentials = array_merge($credentials, $this->getBaseCredentials());
}
public function getBaseCredentials()
{
return array('admin', 'base'),
}
}
class User extends BaseUser
{
public function initCredentials($credentials)
{
$this->credentials = array_merge($credentials, $this->getBaseCredentials());
}
public function getBaseCredentials()
{
return array('user', 'base'),
}
}
The initCredential()
method is the same in both classes, but the getBaseCredential()
method used inside the initCredential()
method is different. First of all, we write a unit test to prevent class behavior.
class UserTest extends PHPUnit_Framework_TestCase
{
public function testGetCredential()
{
$admin = new Admin();
$admin->initCredentials(array('editor'));
$this->assertTrue($admin->hasCredential('base'));
$this->assertTrue($admin->hasCredential('admin'));
$this->assertTrue($admin->hasCredential('editor'));
$this->assertTrue(!$admin->hasCredential('user'));
$user = new User();
$user->initCredentials(array('editor'));
$this->assertTrue($user->hasCredential('base'));
$this->assertTrue($user->hasCredential('user'));
$this->assertTrue($user->hasCredential('editor'));
$this->assertTrue(!$user->hasCredential('admin'));
}
}
Run the test and verify that everything works. Now, before moving the method, we must define the getBaseCredentials()
method as abstract in the super class.
Then we can move the initCredential()
method in the super class and remove it from each subclass. Every time we remove a method we run a unit test.
abstract class BaseUser
{
protected $credentials;
abstract protected function getBaseCredentials();
public function hasCredential($credential)
{
return in_array($credential, $this->credentials);
}
public function initCredentials($credentials)
{
$this->credentials = array_merge($credentials, $this->getBaseCredentials());
}
}
class Admin extends BaseUser
{
protected function getBaseCredentials()
{
return array('admin', 'base'),
}
}
class User extends BaseUser
{
protected function getBaseCredentials()
{
return array('user', 'base'),
}
}
Problem: “You have constructors on subclasses with mostly identical bodies.”
Solution: “Create a super class constructor; call this from the subclass methods.”
The constructor of a class is usually the delegate method to initialize the values of the object instantiated. When we find subclasses that implement the same constructor, we can move it into the super class. In PHP, when we create an instance of a subclass, if the constructor is not implemented, it uses the constructor of the parent class.
If the constructors of the subclasses share only a part of their body, we can extract only this part and put it into a super class constructor, calling the parent constructor inside the subclass constructor. In PHP it is possible to override the parent constructor, changing its interface.
Now, you can have two cases. In the first, you have to extract all of the constructor's body:
In the second case, we have to extract only a part of the subclass's constructor body:
Where we have other equal code to call after the execution of the constructor of the subclass, we can extract a new method and move it in the super class.
We have two classes—News
and Events
—that extend the Article
class. After some refactoring steps we have already moved fields and methods in the super class. But now we have a part of the body of the subclasses' constructor duplicate.
class Article
{
protected $title;
protected $author;
public function getTitle()
{
return $this->title;
}
}
class Event extends Article
{
protected $start_date;
protected $end_date;
public function __construct($title, $author, $start_date, $end_date)
{
$this->title = $title;
$this->author = $author;
$this->start_date = $start_date;
$this->end_date = $end_date;
}
public function getStartDate()
{
return $this->start_date;
}
public function getEndDate()
{
return $this->end_date;
}
}
class News extends Article
{
protected $date;
public function __construct($title, $author, $date)
{
$this->title = $title;
$this->author = $author;
$this->date = $date;
}
public function getDate()
{
return $this->date;
}
}
Before starting the refactoring, we write two unit tests, one for the Event
class and one for the News
class, if we have not already done so. In the unit test we check the behavior of the constructor. We first implement the Event
class unit test.
class EventTest extends PHPUnit_Framework_TestCase
{
public function testEvent()
{
$event = new Event('New event', 'Francesco Trucchia', strtotime('5/10/2010 8:00 am'),
strtotime('5/11/2010 6:00 pm'));
$this->assertTrue($event instanceof Article);
$this->assertEquals('New event', $event->getTitle());
$this->assertEquals(strtotime('5/10/2010 8:00 am'), $event->getStartDate());
$this->assertEquals(strtotime('5/11/2010 6:00 pm'), $event->getEndDate());
}
}
Then we implement a unit test for the News
class.
class NewsTest extends PHPUnit_Framework_TestCase
{
public function testNews()
{
$news = new News('New news', 'Francesco Trucchia', strtotime('5/10/2010 8:00 am'));
$this->assertTrue($news instanceof Article);
$this->assertEquals('New news', $news->getTitle());
$this->assertEquals(strtotime('5/10/2010 8:00 am'), $news->getDate());
}
}
Run the tests and make sure that everything works right. If the tests are running properly, we can begin the steps of refactoring. The first step is to identify the duplicated block of code in the constructor to move to the super class. Looking at the class constructors News
and Events
, we realize that the first two lines are the same. We implement the constructor method in the super class and extract the two lines of code.
class Article
{
...
public function construct($title, $author)
{
$this->title = $title;
$this->author = $author;
}
...
}
Then replace the two lines just copied with the call to the constructor of the super class.
class News extends Article
{
...
public function __construct($title, $author, $date)
{
parent::__construct($title, $author);
$this->date = $date;
}
...
}
class Event extends Article
{
...
public function __construct($title, $author, $start_date, $end_date)
{
parent::__construct($title, $author);
$this->start_date = $start_date;
$this->end_date = $end_date;
}
...
}
We run tests and make sure that everything runs correctly.
Problem: “Behavior on a super class is relevant only for some of its subclasses.”
Solution: “Move it to those subclasses.”
The “Push Down” method is the opposite of the “Move Up” method. We can use this refactoring technique when a certain method in the super class is meaningful only for a certain subclass. In this case you must move the method from the super class to a subclass.
Consider an abstract super class BaseUser
with two subclasses, Premium
and User
.
abstract class BaseUser
{
...
public function getPaymentDate()
{
return $this->payment_date + $this->duration_time;
}
public function haveToPay()
{
return $this->getPaymentDate() < strtotime('today'),
}
...
}
class Premium extends BaseUser
{
...
}
class User extends BaseUser
{
...
}
Methods getPaymentDate()
and haveToPay()
are used only with Premium instances and never with User. So we want to push down these two methods, from the BaseUser
class to the Premium
class. First, write a test to confirm that the behavior doesn't change. We also add a check that methods getPaymentDate()
and haveToPay()
can't be called by the User instance.
--- PremiumTest.php ---
<?php
class PremiumTest extends PHPUnit_Framework_TestCase
{
public function setUp()
{
$this->user = new Premium();
}
public function testHaveToPay()
{
$this->user->duration_time = 60*60*24*30;
$this->user->payment_date = strtotime('5/1/2009'),
$this->assertEquals('31-05-2009', date('d-m-Y', $this->user->getPaymentDate()));
$this->assertTrue($this->user->haveToPay());
$this->user->payment_date = strtotime('+2 days'),
$this->assertFalse($this->user->haveToPay());
}
}
--- UserTest.php ---
class UserTest extends PHPUnit_Framework_TestCase
{
public function setUp()
{
$this->user = new User();
}
public function testHaveToPay()
{
$this->assertTrue($this->user instanceof BaseUser);
$this->assertFalse(method_exists($this->user, 'getPaymentDate'));
$this->assertFalse(method_exists($this->user, 'haveToPay'));
}
}
If we run tests, PremiumTest is ok, and UserTest fails because getPaymentDate()
and haveToPay()
are still methods of the User
class. The next step of refactoring is to implement the getPaymentDate()
and haveToPay()
methods in each subclassing and remove them from the super class.
abstract class BaseUser
{
...
}
class Premium extends BaseUser
{
...
public function getPaymentDate()
{
return $this->payment_date + $this->duration_time;
}
public function haveToPay()
{
return $this->getPaymentDate() < strtotime('today'),
}
...
}
class User extends BaseUser
{
...
public function getPaymentDate()
{
return $this->payment_date + $this->duration_time;
}
public function haveToPay()
{
return $this->getPaymentDate() < strtotime('today'),
}
...
}
At the end, we need to remove the two methods from the User
class, because they don't represent the User
object.
class User extends BaseUser
{
...
}
We run tests and everything works fine.
PHPUnit 3.4.1 by Sebastian Bergmann.
.
Time: 0 seconds
OK (1 test, 3 assertions)
Problem: “A field is used only by some subclasses.”
Solution: “Move the field to those subclasses.”
“Push Down Field” is the opposite of “Move Up Field.” We can use this refactoring technique when a certain attribute in the super class is significant only in one of the subclasses.
After some refactoring steps we have two classes—Developer
and Manager
—that extend the abstract class Employee
.
abstract class Employee
{
protected $project_manager;
protected $assigned_projects;
}
class Developer extends Employee
{
public function setProjectManager($project_manager)
{
$this->project_manager = $project_manager;
}
}
class Manager extends Employee
{
public function setAssignedProjects($assigned_projects)
{
$this->assigned_projects = $assigned_projects;
}
public function getAssignedProjects()
{
return $this->assigned_projects;
}
}
The Employee
class has two properties, $project_manager
and $assigned_projects
. These two parameters don't represent both classes and they need to be moved to subclasses. $project_manager
has to be moved to the Developer
class and $assigned_projects
to the Manager
class.
We write a test to check that the two classes don't have the wrong parameter; if needed, we have to check their behavior to confirm that it doesn't change after refactoring.
--- ManagerTest.php ---
include(dirname(__FILE__).'/Employee.php'),
class ManagerTest extends PHPUnit_Framework_TestCase
{
public function setUp()
{
$this->manager = new Manager();
}
public function testProjectManagerPropertyDoesntExists()
{
$reflection_manager = new ReflectionObject($this->manager);
$this->assertFalse($reflection_manager->hasProperty('project_manager'));
}
}
--- DeveloperTest.php ---
class DeveloperTest extends PHPUnit_Framework_TestCase
{
public function setUp()
{
$this->developer = new Developer();
}
public function testAssignedProjectsPropertyDoesntExists()
{
$reflection_developer = new ReflectionObject($this->developer);
$this->assertFalse($reflection_developer->hasProperty('assigned_projects'));
}
}
Tests fail because the classes still have the wrong properties. To push down fields we need first to copy all the properties we want to move from the super class to the subclasses, and remove them from the super class.
abstract class Employee { }
class Developer extends Employee
{
protected $project_manager;
protected $assigned_projects;
public function setProjectManager($project_manager)
{
$this->project_manager = $project_manager;
}
}
class Manager extends Employee
{
protected $project_manager;
protected $assigned_projects;
public function setAssignedProjects($assigned_projects)
{
$this->assigned_projects = $assigned_projects;
}
public function getAssignedProjects()
{
return $this->assigned_projects;
}
}
At the end we can remove the fields that don't represent the class where they are—for example, we have to remove $project_manager
from the Manager
class, and $assigned_projects
from the Developer
class.
class Developer extends Employee
{
protected $project_manager;
public function setProjectManager($project_manager)
{
$this->project_manager = $project_manager;
}
}
class Manager extends Employee
{
protected $assigned_projects;
public function setAssignedProjects($assigned_projects)
{
$this->assigned_projects = $assigned_projects;
}
public function getAssignedProjects()
{
return $this->assigned_projects;
}
}
We run tests and confirm that everything works fine.
$ phpunit DeveloperTest.php
PHPUnit 3.4.1 by Sebastian Bergmann.
.
Time: 0 seconds
OK (1 test, 1 assertion)
---
$ phpunit ManagerTest.php
PHPUnit 3.4.1 by Sebastian Bergmann.
.
Time: 0 seconds
OK (1 test, 1 assertion
Problem: “A class has features that are used only in some instances.”
Solution: “Create a subclass for that subset of features.”
There are some classes where it's useful to extract a subclass when we realize that a set of its features is used only in certain instances of the class. We have already seen that in other refactoring techniques, it is recommended to extract classes, such as when we have a type attribute because we can remove the attribute adding subclasses or remove it through the state/strategy pattern.
When we don't have the type attribute, we can extract the class or perform this refactoring by extracting a subclass. The leading choice in both cases is to choose between inheritance and delegation. If the object typology needs to change after being instantiated, we must delegate. Otherwise we can use the inheritance.
We have the Subscription
class, which is used both for annual subscriptions and monthly subscriptions. Through the $is_yearly
attribute we can understand whether the subscription is yearly or monthly. The method getPrice()
implements different behaviors based on this attribute.
class Subscription
{
private $price;
private $is_yearly;
private $discount;
public function __construct($price, $is_yearly = false, $discount = 0)
{
$this->price = $price;
$this->is_yearly = $is_yearly;
$this->discount = $discount;
}
public function getPrice()
{
if ($this->is_yearly)
{
$price = $this->price * 12;
return $price - ($price / 100 * $this->discount);
}
return $this->price;
}
public function getDiscount()
{
return $this->discount;
}
public function isYearly()
{
return $this->is_yearly;
}
}
We want to extract a YearSubscription
subclass from the Subscription
class, in order to isolate different behaviors in different classes. Let's start writing a unit test for the Subscription
class.
class SubscriptionTest extends PHPUnit_Framework_TestCase
{
public function testSubscription()
{
$subscription = new Subscription(100);
$this->assertEquals(100, $subscription->getPrice());
$this->assertEquals(false, $subscription->isYearly());
}
public function testYearlysubscription()
{
$yearly_subscription = new Subscription(100, true, 20);
$this->assertEquals((100*12)-((100*12)/100*20), $yearly_subscription->getPrice());
$this->assertEquals(true, $yearly_subscription->isYearly());
}
}
We use a unit test to move through the refactoring steps. First we want the annual subscription to be an instance of a new class called YearSubscription
, so we modify the testYearlysubscription()
method of the unit test, changing the class to instance.
class SubscriptionTest extends PHPUnit_Framework_TestCase
{
...
public function testYearlysubscription()
{
$yearly_subscription = new YearSubscription(100, true, 20);
$this->assertEquals((100*12)-((100*12)/100*20), $yearly_subscription->getPrice());
$this->assertEquals(true, $yearly_subscription->isYearly());
}
...
}
We implement the class YearSubscription
that extends the class Subscription
.
class YearSubscription extends Subscription { }
Now we can change the class interface of Subscription
, because the $discount
attribute belongs only to the YearSubscription
class. To do that, we override the constructor in the YearSubscription
class.
class YearSubscription extends Subscription
{
public function __construct($price, $is_yearly = false, $discount = 0)
{
parent::__construct($price, $is_yearly, $discount);
}
}
Then we modify the interface of the Subscription
class and move the assignment of the $discount
attribute in the constructor of the subclass. We also change the visibility of this attribute from private to protected in the super class, so we can access it in the YearSubscription
subclass.
class Subscription
{
private $price;
private $is_yearly;
protected $discount;
public function __construct($price, $is_yearly = false)
{
$this->price = $price;
$this->is_yearly = $is_yearly;
}
...
}
In the constructor body of the YearSubscription
class, we must change the call to the parent constructor, just modified, and set the $discount
attribute directly.
class YearSubscription extends Subscription
{
public function __construct($price, $discount = 0)
{
parent::__construct($price, true);
$this->discount = $discount;
}
}
Now we can move the methods that are only of the subclass. The method getDiscount()
of the Subscription
class is used only with the class YearSubscription
, so push it down.
class YearSubscription extends Subscription
{
public function __construct($price, $discount = 0)
{
parent::__construct($price, true);
$this->discount = $discount;
}
public function getDiscount()
{
return $this->discount;
}
}
We also push down the attribute $discount
of the super class that is used only with the subclass.
class YearSubscription extends Subscription
{
protected $discount;
public function __construct($price, $discount = 0)
{
parent::__construct($price, true);
$this->discount = $discount;
}
public function getDiscount()
{
return $this->discount;
}
}
We also remove the $is_yearly
attribute that represents the same information given by inheritance. To do this we must change the method isYearly()
in the super class to return a constant value. The same thing should be done also in the subclass.
class Subscription
{
...
public function isYearly()
{
return false;
}
}
class YearSubscription
{
...
public function isYearly()
{
return true;
}
}
To remove the $is_yearly
attribute, we must first remove the conditional logic of the method getPrice()
with the polymorphism. In practice, we modify the method in the super class and then we do an override in the subclass. We must also change the visibility of the attribute price, otherwise the subclass cannot access it.
class Subscription
{
protected $price;
...
public function getPrice()
{
return $this->price;
}
}
class YearSubscription
{
...
public function getPrice()
{
$price = $this->price * 12;
return $price - ($price / 100 * $this->discount);
}
}
Now we can remove all references to the $is_yearly
attribute in the Subscription
class.
class Subscription
{
protected $price;
public function __construct($price)
{
$this->price = $price;
}
public function getPrice()
{
return $this->price;
}
public function isYearly()
{
return false;
}
}
Remember to run the tests at every step of refactoring to verify that the correct behavior is maintained.
Problem: “You have two classes with similar features.”
Solution: “Create a super class and move the common features to the super class.”
When we have two classes doing the same things in the same way, or similar things in different ways, we can extract a super class and standardize some of their behavior to avoid duplicated code.
Once we have extracted the super class, we can use all the refactoring techniques seen in this chapter to remove duplicated code. We can push attributes and methods up or down, we can rename the methods that do the same thing but are named differently and move them to the super class, we can pull up the constructor, and so on.
We can also use inheritance to remove duplication. When we cannot use inheritance, we can use delegation. It isn't always easy to know at first what is better to use. Don't worry—we use a KISS approach (http://en.wikipedia.org/wiki/K.I.S.S
.), and if we are wrong, we can always refactor, replacing inheritance with delegation and vice versa.
We have two classes—Order
and Estimate
—that have the same behavior.
class Order
{
private $id;
private $number;
private $details = array();
public function setId($id)
{
$this->id = $id;
}
public function setNumber($number)
{
$this->number = $number;
}
public function addDetail($detail)
{
$this->details[] = $detail;
}
public function getTotal()
{
$total = 0;
foreach($this->details as $detail)
{
$total += $detail->getTotal();
}
return $total;
}
}
class Estimate
{
private $id;
private $number;
private $details = array();
public function setId($id)
{
$this->id = $id;
}
public function setNumber($number)
{
$this->number = $number;
}
public function addDetail($detail)
{
$this->details[] = $detail;
}
public function getTotal()
{
$total = 0;
foreach($this->details as $detail)
{
$total += $detail->getTotal();
}
return $total;
}
}
We want to extract the super class Document
in order to remove the duplication of code, because both classes have the same attributes and the same methods. First of all, we write a unit test for each class.
class DocumentIntegrationTest extends PHPUnit_Framework_TestCase
{
private function detail($price, $amount, $description)
{
$detail = new Detail();
$detail->setPrice($price);
$detail->setAmount($amount);
$detail->setDescription($description);
return $detail;
}
public function testOrder()
{
$order = new Order();
$order->setId(10);
$order->setNumber(100);
$order->addDetail($this->detail(100, 10, 'Detail 1'));
$this->assertEquals(1000, $order->getTotal());
}
public function testEtimate()
{
$estimate = new Estimate();
$estimate->setId(10);
$estimate->setNumber(100);
$estimate->addDetail($this->detail(100, 10, 'Detail 1'));
$this->assertEquals(1000, $estimate->getTotal());
}
}
?>
</pre>
Create a super class Document
and make sure that the classes Order
and Estimate
extend it. Add a test that checks the inheritance to the unit test.
class Document {}
class Order extends Document { ... }
class Estimate extends Document { ... }
class DocumentIntegrationTest extends PHPUnit_Framework_TestCase
{
public function testOrder()
{
...
$this->assertTrue($order instanceof Document);
$this->assertEquals(1000, $order->getTotal());
}
public function testEtimate()
{
...
$this->assertTrue($estimate instanceof Document);
$this->assertEquals(1000, $estimate->getTotal());
}
}
We run the unit test and verify it is correct. If everything is ok, we can begin to pull up the attributes of the duplicated classes in the super class Document
, changing their visibility where needed. In our case we pull up the $id
, $number
, and $details
attributes, changing their visibility from private to protected. Once moved in the super class, we can remove them from the subclasses.
class Document
{
protected $id;
protected $number;
protected $details = array();
}
Then pull up the methods from the subclasses to the super class, one at a time, and run the unit tests at each time. After moving the method, remove it from the other subclasses.
class Document
{
protected $id;
protected $number;
protected $details = array();
public function setId($id)
{
$this->id = $id;
}
public function setNumber($number)
{
$this->number = $number;
}
public function addDetail($detail)
{
$this->details[] = $detail;
}
public function getTotal()
{
$total = 0;
foreach($this->details as $detail)
{
$total += $detail->getTotal();
}
return $total;
}
}
class Estimate extends Document { }
class Order extends Document { }
Problem: “A super class and subclass are not very different.”
Solution: “Merge them together.”
When we realize that a single class hierarchy doesn't give value to any of the classes in the hierarchy, we can do a merge of the hierarchy and create a single class. If we are often moving methods or attributes up and down the hierarchy, it probably means that we can't give the right meaning to any of the classes in the hierarchy. So we must merge them.
We have a Salesman
class that extends an abstract class, Employee
. The class Salesman
is the only one that extends the Employee
class, so we want to remove the Employee
class, pushing down all methods and properties merging the hierarchy.
abstract class Employee
{
protected $firstname;
protected $lastname;
}
class Salesman extends Employee
{
public function setFirstname($firstname)
{
$this->firstname = $firstname;
}
public function setLastname($lastname)
{
$this->lastname = $lastname;
}
public function getFirstname()
{
return $this->firstname;
}
public function getLastname()
{
return $this->lastname;
}
}
We write a test for the Salesman
class, to confirm its behavior doesn't change after merging.
class CollapseHierarchyTest extends PHPUnit_Framework_TestCase
{
public function testCollapse()
{
$salesman = new Salesman();
$salesman->setFirstname('Francesco'),
$salesman->setLastname('Trucchia'),
$this->assertEquals('Francesco', $salesman->getFirstname());
$this->assertEquals('Trucchia', $salesman->getLastname());
}
}
The Employee
class has only two properties, and we move them to Salesman
with the “Push Down Field” technique.
abstract class Employee
{
}
class Salesman extends Employee
{
protected $firstname;
protected $lastname;
public function setFirstname($firstname)
{
$this->firstname = $firstname;
}
public function setLastname($lastname)
{
$this->lastname = $lastname;
}
public function getFirstname()
{
return $this->firstname;
}
public function getLastname()
{
return $this->lastname;
}
}
Then, we remove the hierarchy from Salesman
and run the unit test.
class Salesman
{
protected $firstname;
protected $lastname;
public function setFirstname($firstname)
{
$this->firstname = $firstname;
}
public function setLastname($lastname)
{
$this->lastname = $lastname;
}
public function getFirstname()
{
return $this->firstname;
}
public function getLastname()
{
return $this->lastname;
}
}
If the test is ok, we remove the Employee
class also and run all unit tests again.
Problem: “You have two methods in subclasses that perform similar steps in the same order, yet the steps are different.”
Solution: “Get the steps into methods with the same signature, so that the original methods become the same. Then you can pull them up."
There are cases in which removing code duplication between the methods of our hierarchies may seem impossible, since these methods differ very little on features, but enough to not be able to pull methods and/or move them to the super class.
One of these cases is when we have two methods that perform operations in the same order, but the operations are different, sometimes so quite. The Gang of Four, to solve this problem, introduced the “Template Pattern”[GOF]. In essence, the pattern is to implement a method in the super class that calls each step in sequence through methods that may be on the super class or subclass. At this point, the methods of the steps, through polymorphism, can be quite different. The important thing is that they have the same interface.
The Article
class provides two methods for printing the article in HTML or TEXTILE format (http://en.wikipedia.org/wiki/Textile_%28markup_language%29
).
class Article
{
...
public function texttileView()
{
$output = 'h1. '.$this->getTitle().PHP_EOL.PHP_EOL;
$output .= $this->getIntro().PHP_EOL.PHP_EOL;
$output .= $this->getBody().PHP_EOL.PHP_EOL;
$output .= '_Written by '.$this->getAuthor().' on '.date('m/d/Y', $this->getDate()).'_';
return $output;
}
public function htmlView()
{
$output = '<h2>'.$this->getTitle().'</h2>'.PHP_EOL;
$output .= '<p>'.$this->getIntro().'</p>'.PHP_EOL;
$output .= '<p>'.$this->getBody().'</p>'.PHP_EOL;
$output .= '<em>Written by '.$this->getAuthor().' on '.date('m/d/Y',
$this->getDate()).'</em>';
return $output;
}
}
These two methods are very similar because they implement the same steps, but each step is very different from each other. Therefore we want to remove the duplication of some code introducing the “Template Pattern” [GOF]. Before starting our refactoring, we extract a new class from the article class, which will be delegated only to print the template.
Extract the ArticleView
class from the Article
class that implements only the textileView()
and htmlView()
methods.
class ArticleView
{
public function textileView($article)
{
$output = 'h1. '.$article->getTitle().PHP_EOL.PHP_EOL;
$output .= $article->getIntro().PHP_EOL.PHP_EOL;
$output .= $article->getBody().PHP_EOL.PHP_EOL;
$output .= '_Written by '.$article->getAuthor().' on '.date('m/d/Y', $article->getDate()).'_';
return $output;
}
public function htmlView($article)
{
$output = '<h2>'.$article->getTitle().'</h2>'.PHP_EOL;
$output .= '<p>'.$article->getIntro().'</p>'.PHP_EOL;
$output .= '<p>'.$article->getBody().'</p>'.PHP_EOL;
$output .= '<em>Written by '.$article->getAuthor().' on '.date('m/d/Y',
$article->getDate()).'</em>';
return $output;
}
}
class Article
{
...
public function textileView()
{
$view = new ArticleView();
return $view->textileView($this);
}
public function htmlView()
{
$view = new ArticleView();
return $view->htmlView($this);
}
...
}
From the ArticleView
class, we can extract two subclasses, ArticleTextile
and ArticleHtml
. One implements the method textileView()
, and the other implements the method htmlView()
. As the method name would be redundant with the class name, rename both methods in view()
. Once the class is extracted, we also fix the calls to the class on clients.
class ArticleView
{
}
class ArticleTextile extends ArticleView
{
public function view($article)
{
$output = 'h1. '.$article->getTitle().PHP_EOL.PHP_EOL;
$output .= $article->getIntro().PHP_EOL.PHP_EOL;
$output .= $article->getBody().PHP_EOL.PHP_EOL;
$output .= '_Written by '.$article->getAuthor().' on '.date('m/d/Y', $article->getDate()).'_';
return $output;
}
}
class ArticleHtml extends ArticleView
{
public function view($article)
{
$output = '<h2>'.$article->getTitle().'</h2>'.PHP_EOL;
$output .= '<p>'.$article->getIntro().'</p>'.PHP_EOL;
$output .= '<p>'.$article->getBody().'</p>'.PHP_EOL;
$output .= '<em>Written by '.$article->getAuthor().' on '.date('m/d/Y',
$article->getDate()).'</em>';
return $output;
}
}
class Article
{
...
public function textileView()
{
$view = new ArticleTextile();
return $view->view($this);
}
public function htmlView()
{
$view = new ArticleHtml();
return $view->view($this);
}
...
}
?>
</pre>
We run the tests and make sure that everything is correct. At this point we can start implementing the “Form Template” technique for the view()
method of the ArticleTextile
and ArticleHtml
classes. Split the method into steps so that all the steps are separated. We start from the class ArticleTextile
.
class ArticleTextile extends ArticleView
{
protected function title($article)
{
return 'h1. '.$article->getTitle().PHP_EOL.PHP_EOL;
}
protected function intro($article)
{
return $article->getIntro().PHP_EOL.PHP_EOL;
}
protected function body($article)
{
return $article->getBody().PHP_EOL.PHP_EOL;
}
protected function footer($article)
{
return '_Written by '.$article->getAuthor().' on '.date('m/d/Y', $article->getDate()).'_';
}
public function view($article)
{
return $this->title($article)
.$this->intro($article)
.$this->body($article)
.$this->footer($article);
}
}
We do the same thing for the ArticleHtml
class.
class ArticleHtml extends ArticleView
{
protected function title($article)
{
return '<h2>'.$article->getTitle().'</h2>'.PHP_EOL;
}
protected function intro($article)
{
return '<p>'.$article->getIntro().'</p>'.PHP_EOL;
}
protected function body($article)
{
return '<p>'.$article->getBody().'</p>'.PHP_EOL;
}
protected function footer($article)
{
return '<em>Written by '.$article->getAuthor().' on '.date('m/d/Y',
$article->getDate()).'</em>';
}
public function view($article)
{
return $this->title($article)
.$this->intro($article)
.$this->body($article)
.$this->footer($article);
}
}
Finally we can move the view()
method in the super class ArticleView
, and remove it from the subclasses.
class ArticleView
{
public function view($article)
{
return $this->title($article)
.$this->intro($article)
.$this->body($article)
.$this->footer($article);
}
}
class ArticleTextile extends ArticleView
{
protected function title($article)
{
return 'h1. '.$article->getTitle().PHP_EOL.PHP_EOL;
}
protected function intro($article)
{
return $article->getIntro().PHP_EOL.PHP_EOL;
}
protected function body($article)
{
return $article->getBody().PHP_EOL.PHP_EOL;
}
protected function footer($article)
{
return '_Written by '.$article->getAuthor().' on '.date('m/d/Y', $article->getDate()).'_';
}
}
class ArticleHtml extends ArticleView
{
protected function title($article)
{
return '<h2>'.$article->getTitle().'</h2>'.PHP_EOL;
}
protected function intro($article)
{
return '<p>'.$article->getIntro().'</p>'.PHP_EOL;
}
protected function body($article)
{
return '<p>'.$article->getBody().'</p>'.PHP_EOL;
}
protected function footer($article)
{
return '<em>Written by '.$article->getAuthor().' on '.date('m/d/Y',
$article->getDate()).'</em>';
}
}
We run the unit test for the last time and make sure that everything works fine.
Problem: “A subclass uses only part of a super class's interface or does not want to inherit data.”
Solution: “Create a field for the super class, adjust methods to delegate to the super class, and remove the subclass.”
Inheritance is a very important feature of object-oriented programming. We must use it in the right way, otherwise it could create confusion. When we have classes that use few methods of the super class, classes that are not represented in the data and methods of their super class, or represented only partially, we must ask whether the choice of inheritance is the right choice.
Sometimes, it can be right anyway, but others may be better off using delegation rather than inheritance. Through delegation we explain in a better way what we need of the delegated class, how it represents our class, and what we can ignore. There is a payable fee, which is rewriting many methods of the delegated class, but, on the other hand, the advantage is that it increases the code's clarity.
The class Car
extends the class Engine
, but it uses only one attribute of this class, the attribute $CV
. So the Engine
class is only partially described from the Car
class. In fact, the engine should be an attribute of the Car
class and not its super class, since there is a more general set of the car. We can confirm that in this case it would be better to use delegation rather than inheritance.
class Engine
{
protected $fuel_type;
protected $kW;
protected $CV;
public function getFuel()
{
return $this->fuel;
}
public function getKW()
{
return $this->kW;
}
public function getCV()
{
return $this->CV;
}
public function setFuel($fuel)
{
$this->fuel = $fuel;
}
public function setKW($kw)
{
$this->kW = $kw;
}
public function setCV($cv)
{
$this->CV = $cv;
}
}
class Car extends Engine
{
protected $brand;
protected $model;
public function __toString()
{
return $this->brand.' '.$this->model.' ('.$this->getCV().'CV)';
}
public function setModel($model)
{
$this->model = $model;
}
public function getModel()
{
return $this->model;
}
public function setBrand($brand)
{
$this->brand = $brand;
}
public function getBrand()
{
return $this->brand;
}
}
Following the mechanism, we must first write a unit test for the Car
subclass.
class CarTest extends PHPUnit_Framework_TestCase
{
public function testEngine()
{
$car = new Car();
$car->setBrand('Audi'),
$car->setModel('A3'),
$car->setCV('250'),
$this->assertEquals('Audi', $car->getBrand());
$this->assertEquals('A3', $car->getModel());
$this->assertEquals('250', $car->getCV());
$this->assertEquals('Audi A3 (250CV)', (string)$car);
}
}
We run the test and verify that it doesn't fail. To replace inheritance, we must, first of all, create an attribute $engine
in the Car
class that is an instance of itself.
class Car extends Engine
{
...
protected $engine;
public function __construct()
{
$this->engine = $this;
}
...
}
Replace, in the methods of the Car
class, the calls to methods or attributes of the super class with the delegated attribute. In our case, the only method that makes use of attributes of the super class is the __toString()
method.
class Car extends Engine
{
...
public function __toString()
{
return $this->brand.' '.$this->model.' ('.$this->engine->getCV().'CV)';
}
...
}
Remove inheritance from the Car
class and assign to the $engine
attribute a new instance of object engine
.
class Car
{
...
protected $engine;
public function __construct()
{
$this->engine = new Engine();
}
...
}
Finally, add the proxy method setCV()
in the Car
class to set the attribute $CV
of the Engine
class.
class Car
{
...
public function setCV($cv)
{
$this->engine->setCV($cv);
}
...
}
Run the unit test and verify that everything is correct.
Problem: “You're using delegation and are often writing many simple delegations for the entire interface.”
Solution: “Make the delegating class a subclass of the delegate.”
This technique is the opposite of the technique “Replace Inheritance with Delegation.” When a class uses all the methods of a delegate class, and when we add a new method in the delegate class, we must always add it in the delegating class. We can “Replace Delegation with Inheritance.”
If the delegating class doesn't use all the methods of the delegate class, we cannot use this refactoring technique, because with inheritance all methods of the super class become interfaces for subclasses. If we cannot replace the delegation, we can use other methods. For example, we can directly make access clients to the delegated class, so we don't have to rewrite all delegate methods, or we can extract a super class from the delegate class to isolate the common interface and then inherit from it.
If the delegate class is shared with other objects and may change after you have instantiated it, we cannot replace it with inheritance, because then we couldn't share it.
We have an Employee
class, without attributes, which delegates all its behaviors to the Person
class.
class Person
{
protected $name;
public function setName($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
public function getLastName()
{
list(,$lastname) = explode(' ', $this->getName());
return $lastname;
}
}
class Employee
{
protected $person;
public function __construct()
{
$this->person = new Person();
}
public function __toString()
{
return 'Emp: '.$this->person->getLastName();
}
public function getName()
{
return $this->person->getName();
}
public function setName($name)
{
$this->person->setName($name);
}
}
Write a unit test to confirm that the behaviors remain unchanged at the end of refactoring.
class EmployeeTest extends PHPUnit_Framework_TestCase
{
public function testEngine()
{
$employee = new Employee();
$employee->setName('Francesco Trucchia'),
$this->assertEquals('Emp: Trucchia', (string)$employee);
$this->assertEquals('Trucchia', $employee->getLastName());
$this->assertEquals('Francesco Trucchia', $employee->getName());
}
}
Define inheritance from the Person
class in the Employee
class.
class Employee extends Person {...}
We run the tests and make sure everything is still correct. Afterwards, we assign an instance of the object itself to the delegate attribute and remove accessory methods.
class Employee extends Person
{
protected $person;
public function __construct()
{
$this->person = $this;
}
public function __toString()
{
return 'Emp: '.$this->person->getLastName();
}
}
At this point, we can directly call the methods of the Person
class and remove the delegate attribute.
class Employee extends Person
{
public function __toString()
{
return 'Emp: '.$this->getLastName();
}
}
Run the tests and verify that the behaviors don't change.
In this chapter we learned how to simplify and modify our class hierarchies. We learned how to move methods and properties up and down in hierarchical classes, how to collapse unnecessary hierarchies, and how to extract super classes or subclasses in the presence of over-responsible classes. Learning how to manage generalization facilitates a very simple and easily maintainable code.
18.119.167.173