Refactoring - A first example
Posted on 30/4/07 by Tim Koschützki
Today we will look at a first php example of refactoring, to start creating a sense for it.
Think about the following scenario: You want to dynamically add workers to a department and then display that department's workers as an unordered list. Pretty easy eh? However, we will discover that there are some pitfalls and ways to improve from our initial code. We'll start off with the following:
protected $name;
protected $workers = array();
public function addWorker($name,$isChief,$salary) {
$this->workers[] = array($name,$isChief,$salary);
}
public function listWorkers() {
echo '<ul>';
foreach($this->workers as $worker) {
echo "<li>{$worker[0]} - {$worker[1]} - {$worker[2]}";
}
echo '</li></ul>';
}
}
$dept = new Department;
$dept->addWorker('Felix Broda',1,2000);
$dept->addWorker('Stefan Muster',0,1400);
$dept->addWorker('Klaus Schmidt',0,1350);
$dept->listWorkers();
There are a couple of really bad Code Smellsin it, unfortunately.
Our first refactoring
The first thing that comes to mind is the very bad way of storing the workers in the Department class. I mean an array is by all means fine. However, look at the 13th line: We will always depend on our implementation and have to go to the addWorker function to remember the order in which we stored the data. How can we fix this? Introduce a class for a Worker:
protected $name;
protected $isChief = false;
protected $salary = 0;
}
Now we can change the addWorker method of the Department class:
$this->workers[] = $worker;
}
Okay, so we can change the client code now in the listWorkers() method:
echo '<ul>';
foreach($this->workers as $worker) {
echo "<li>{$worker->name} - {$worker->isChief} - {$worker->salary}";
}
echo '</li></ul>';
}
Okay looking good now. :)
Our second refactoring
When executing the code we see that the attributes of the Worker class must be public in order to work. A quick refactoring, let's fix it up:
public $name;
public $isChief = false;
public $salary = 0;
}
Making our list standards-compliant
There are actually two smaller problems now with our list generation: We are missing the closing list-tags:
Okay this took us 5 seconds, but it was worth it. Now we notice that when we are outputting a department-list before adding workers to it, we get an unordered list that has starting ul-tags, but no li-tags. This is not standards-compliant, so we fix it:
if(count($this->workers) > 0) {
echo '<ul>';
foreach($this->workers as $worker) {
echo "<li>{$worker->name} - {$worker->isChief} - {$worker->salary}";
}
echo '</li></ul>';
} else {
echo '<p>Sorry, there are no workers to display.</p>';
}
}
Switching hats again - adding a feature
Switching hats now, because our customer told us, that he wants the number of department workers displayed at the end of each list. We are wearing our feature-hat now. This feature is a quick thing, so we implement it right away:
if(count($this->workers) > 0) {
echo '<ul>';
foreach($this->workers as $worker) {
echo "<li>{$worker->name} - {$worker->isChief} - {$worker->salary}";
}
echo '</li></ul>';
echo "<p>There are {count($this->workers} workers in this department.</p>";
} else {
echo '<p>Sorry, there are no workers to display.</p>';
}
}
More refactoring
Looking at our code now we use the count() function twice to receive the same bit of information. Now we could call count() once and store its result in a variable. However, that would tie our interface to the implementation. Let's add a new method instead that counts the number of workers for us:
Our altered listWorkers() method looks as follows:
$numWorkers = $this->numWorkers();
if($numWorkers > 0) {
echo '<ul>';
foreach($this->workers as $worker) {
echo "<li>{$worker->name} - {$worker->isChief} - {$worker->salary}";
}
echo '</li></ul>';
echo "<p>There are {$numWorkers} workers in this department.</p>";
} else {
echo '<p>Sorry, there are no workers to display.</p>';
}
}
Cool stuff. :) Let's see what we currently have:
protected $name;
protected $workers = array();
public function addWorker($worker) {
$this->workers[] = $worker;
}
public function listWorkers() {
$numWorkers = $this->numWorkers();
if($numWorkers > 0) {
echo '<ul>';
foreach($this->workers as $worker) {
echo "<li>{$worker->name} - {$worker->isChief} - {$worker->salary}";
}
echo '</li></ul>';
echo "<p>There are {$numWorkers} workers in this department.</p>";
} else {
echo '<p>Sorry, there are no workers to display.</p>';
}
}
public function numWorkers() {
return count($this->workers);
}
}
class Worker {
public $name;
public $isChief = false;
public $salary = 0;
}
Adding some client code into the mix
$felix->name = 'Felix Broda';
$felix->isChief = true;
$felix->salary = 2000;
$stefan = new Worker;
$stefan->name = 'Stefan Muster';
$stefan->salary = 1400;
$klaus = new Worker;
$klaus->name = 'Klaus Schmidt';
$klaus->salary = 1350;
$dept = new Department;
$dept->addWorker($felix);
$dept->addWorker($stefan);
$dept->addWorker($klaus);
$dept->listWorkers();
Now it's all working well, but darn that client code is so much for what it does. Let's refactor a bit more and add a handy constructor to the Worker class:
public $name;
public $isChief = false;
public $salary = 0;
public function __construct($name,$salary,$isChief=false) {
$this->name = $name;
$this->salary = $salary;
$this->isChief = $isChief;
}
}
Note that we put the isChief variable as an optional parameter with the default value of false. This is handy, because most workers will not be chiefs, leaving us without the need to explicitely tell every new worker object that it is not a chief.
Now our new client code:
$stefan = new Worker('Stefan Muster',1400);
$klaus = new Worker('Klaus Schmidt',1350);
$dept = new Department;
$dept->addWorker($felix);
$dept->addWorker($stefan);
$dept->addWorker($klaus);
$dept->listWorkers();
Ah many less lines - much better. :)
Shortening the code more
Let's add the ability to add an array of workers all at once. Our goal is to make the following line valid:
Our altered addWorker() method:
if(is_array($worker)) {
foreach($worker as $w)
$this->workers[] = $w;
} else {
$this->workers[] = $worker;
}
}
Now was this a feature or a refactoring?
Adding tax rates
Our client wants us to display the actual salaries of all workers of a department. We add the feature in our listWorkers() method:
$numWorkers = $this->numWorkers();
if($numWorkers > 0) {
echo '<ul>';
foreach($this->workers as $worker) {
$salary *= 1.07;
echo "<li>{$worker->name} - {$worker->isChief} - {$salary}";
}
echo '</li></ul>';
echo "<p>There are {$numWorkers} workers in this department.</p>";
} else {
echo '<p>Sorry, there are no workers to display.</p>';
}
}
However, we just introduced another Code Smell. We should not hard code the calculation of the sales tax into our client code. If the calculation changes we would have to adjust all client code that depends on it. Let's make the overall flow by introducing a method in the Worker class clearer:
Now we can rewrite listWorkers() as follows:
$numWorkers = $this->numWorkers();
if($numWorkers > 0) {
echo '<ul>';
foreach($this->workers as $worker) {
echo "<li>{$worker->name} - {$worker->isChief} - {$worker->calcTotalSalary()}";
}
echo '</li></ul>';
echo "<p>There are {$numWorkers} workers in this department.</p>";
} else {
echo '<p>Sorry, there are no workers to display.</p>';
}
}
Introducing a constant for the tax rate
Now tax rates change and our code should account for them. Let's introduce a constant for the tax rate to save us a lot of trouble changing client code later when the tax rate changes:
class Worker {
public $name;
public $isChief = false;
public $salary = 0;
public function __construct($name,$salary,$isChief=false) {
$this->name = $name;
$this->salary = $salary;
$this->isChief = $isChief;
}
public function calcTotalSalary() {
return ($this->salary + TAX_RATE/100 * $this->salary);
}
}
Conclusion
As you see when you refactor you make little changes to code that is working already, but that can be improved to quite some extent. Hopefully you noticed how we switched hats often in order to
reach to some very cool and working code. It often takes only fundamental sense of architecture to get to clean code that works. Please keep in mind that you do not add a feature and refactor at the same time, as that will lead you to the road to hell.
Happy refactoring. :)
You can skip to the end and add a comment.
Yeah, I especially like the throurough code examples.
Good tutorial.
Finally somebody writing some thoughts and insights on refactoring in PHP. I think you should have mentioned at least to run the according developer test after every large refactoring step, to prove the behaviour preservation. Thanks for posting on this topic.
Hi Raphael,
yes you are right. I could have highlighted this part of refactoring a bit more. Thanks for the tip.
I like the tutorial. It is to bad that your website is not displaying correctly on a Mac using Safari. It makes it hard to read.
Hi Oscar, thanks for your comment.
I will work on it. :) Promise!
This post is too old. We do not allow comments here anymore in order to fight spam. If you have real feedback or questions for the post, please contact us.
Thanks for this nice tutorial. :)