- An object is a set of data and processes that computer the data.
- 2 basic data types: primitive and structured. Represented with associate arrays in PHP.
- I need to replace data values with objects.
- Remove all speculative code - use only what you need today.
- Encapsulation field - setter/getter class. Expose their attributes (potentially problematic).
- Encapsulate the collection then remove setting methods.
- Big refactoring - change from procedural to object-oriented code is worth the effort?
- You can chain functions to make a sentence: $this->order->getCustomer()->makeSilver()
The functions in the book were short - 1 or 2 lines. Very simple. Use abstract and encapsulation.
"...[Create] a small context to cope with when reading, understanding, designing and writing our code."
Chapter 2
"Anyway, in real-life projects, as well, we come to a point when we have to decide whether to stop refactoring a given portion of code, and whether to invest time in improving existing code or developing new features. The right skills to decide this are to be derived from the context with the help of experience. By now what we care about most is that you met your first refactoring. " --3.5
4.3. When You Shouldn't Do Refactoring
From my experience I can tell you that there are no contraindications for doing refactoring, so my advice is to do it always and often. It should become an activity of your daily production process. If we put refactoring in our toolbox, our code will always be fragrant.
However, if we aren't a refactoring expert and we have to refactor very complex code that is untested and full of bugs, written by someone else, in this case we should strongly consider rewriting it from scratch. The risk of rewriting software, however, is to miss the value that the software already has for our customers and users who currently use it. Before rewriting the software from scratch, you need to interview all users to figure out what features they use and how, so you can implement all features including those hidden.
4.4. Some Simple Rules
The rules to follow when refactoring are few but strict. They must be observed to avoid the risk of decreasing the software's value and wasting time.
-
Test before refactoring.
-
Make small and simple changes.
-
Never change functionality.
-
Follow the bad smells.
-
Follow refactoring techniques step-by-step.
4.4.1. Test Before Refactoring
The first rule, and, in my opinion, the most important, means that we cannot do refactoring in a piece of our code if that piece of code is not tested. The refactoring work does not have to change the behavior of code, but it should improve code executing the same behavior. In writing a test, we cage the behavior, making sure, first, that it is correct, and that the refactoring work will be finished only when the tests become green again.
If we're applying refactoring techniques that introduce new units in our code, remember to write new tests for these new units before creating them.
Just by performing the tests, we are able to measure whether the behavior of the software has changed, and fix it, if needed. We have to see the test as the main tool in our toolbox, which gives us instant feedback if something goes wrong. Our minds are often unable to remember how units communicate within the system, and often a simple change can affect the whole system. But the test may notify us of the malfunction before it is put into production. In this way we entrust the responsibility for reporting errors to an external, automatic, and repeatable tool, which will always work in the same way, instead of our minds, which often fail.
4.4.2. Small and Simple Changes
Refactoring is a low-level task. When we perform it, we are constantly working with the heart of our software system. It's a bit like performing surgery to improve something in your body that no longer works well—it is always a very delicate activity.
That is why we always have to create as much value as possible and be able to go back, if we are going the wrong way. Sometimes the changes we make can create more problems than benefits. For example, we can pursue a design that does not fit our needs, and we have to go back quickly because our changes are incorrect. To be able to go back easily, we must make small and clear steps, verifying that at each step, tests are still green. Thus we ensure the highest value, and we can more easily go back, when needed.
The refactoring process is an iterative process that consists of the following steps:
-
Find the piece of code to change.
-
Write a unit test if you didn't.
-
Do a small step to improve.
-
Run the test and fix the code until the test is not green.
-
Go back to step 3.
Sometimes you may be tempted to iterate too many times, trying to find the perfect code. The code is almost never perfect, but there is a code right now. This is what we pursue, because if it goes wrong tomorrow, we can do refactoring.
We must learn when to stop—we can't iterate the process of refactoring an infinite number of times, because there is always a delivery date that we must respect.
Sometimes it can seem like the value of the software is not increasing with these small steps. You always need to remember that the activity of refactoring is an everyday activity that gives results over time, because the whole value is made up of small changes.
4.4.3. Never Change Functionality
All the refactoring techniques are conservative techniques that aim to improve our code in performing the procedures it already runs. Refactoring does not need to add new functionalities, but to modify the existing code to better accommodate new features. When we add new features to our software, we can't begin with development and refactoring while we are adding the features. We can do refactoring only before or after. If we immediately realize that the design of our software is incorrect, we must modify it immediately. If we realize late that the design is incorrect, we can add the features and then do refactoring of the code, making it more suited to the features just included.
If you want to do a proper refactoring and, at the same time, maintain the most value for your customer, try to follow this rule. Otherwise, you could spend some bad nights trying to find bugs.
4.4.4. Follow the Bad Smells
In Chapter 2 we learned how to recognize bad smells. Refactoring assumes that there are some bad smells; if there aren't, we can't do refactoring. I have seen some developers do refactoring to improve design, simply because the finished design was unexpected.
When design emerges through TDD techniques, sometimes it happens that the design implemented is different from the one expected. In this case, doing refactoring won't increase value. If the code is correct, the test is green, the customer has accepted the functionalities implemented, and there are no bad smells, there isn't a reason to do refactoring. The only important thing is that the code must be tested in order to change it, as soon as we realize that we want to change it.
When we're not sure if our code has any bad smells, never mind—simply test the code and move on. If things should be changed, they will be obvious, and at that point you will have no doubts about what to do.
Metaphorically speaking, we must use smell rather than sight. Sometimes, our eyes can deceive us in refactoring. Just follow the smell—if there isn't one, we can move on with our work.
4.4.5. Follow Refactoring Techniques Step-by-Step
In the next chapters, we will see a catalogue of the main refactoring techniques created by Martin Fowler [FOW01]. Each technique will be accompanied by practical reasons, a step-by-step mechanism, and some examples. The rule is to follow all the steps as described.
We must learn to recognize bad smells and to identify which techniques to apply. Once we have identified the technique, we need to follow the steps of the mechanism, because these steps are small enough that we can go back easily if needed and not get lost in the difficult art of refactoring.
The goal is to become autonomous and know the technique by memory. Then refactoring will be automated and applied in our daily coding.
Chapter 5. Test-First Development
Refactoring is a powerful tool, enabling you to improve the design of your software. It means taking your working code and changing it, sometimes deeply, to make it perform the same tasks it performed before changing it. Somehow this could sound quite pointless: why be at risk of losing a piece of working code just to improve its design? Shouldn't the functionality of working code be valued more than its structure?
Any wise programmer should ask herself questions like these at the first mention of refactoring. We have already called attention many times to the importance of preserving our existing code base and why this should be our main goal. Then how can we change it without introducing errors? How should I explore new ways to arrange my software? Is it possible for me to refactor my code protected by a strong and soft safety net? Yes, it is, because automated tests are at hand, one of the most powerful tools ever in the history of software development.
5.1. Building Value One-Way
You have been coding thousands and thousands of lines of code for years, and you have spent thousands of hours debugging that code, but how often did you stop and think about how unproductive debugging your code is? Yes, we really mean it: debugging is not productive. Debugging is just a way to finally deliver what you were already supposed to have delivered.
Even the best of our customers is willing to pay to get some technical value to invest in her business. In a healthy environment the customer should also try to maximize her return on investment (ROI) by getting the most value for the lowest expense, taking into consideration any short-term goal together with longer-term ones. In the same healthy environment we should support these needs by trying to maximize our ROI by reducing costs, not by delivering lower quality.
Debugging is a step back from a healthy developer-customer system because debugging is always a cost. Even on a fixed price contract featuring a warranty option on bugged features, the customer will pay a cost for debugging, because a dollar lost now is not paid back by the same dollar next week. If debugging were considered a standard practice in the automotive market, we would be buying cars incapable of bringing us home 60–70% of the time, attached to warranties providing us with ones that actually function. It can happen, we know, but I bet you wouldn't be pleased to see it happen 60–70% of the time. Those numbers are considered low in the software industry indeed!
Obviously this way of conceiving software development has its roots in the reality of the software industry and engineering, and no one should be faulted for debugging code if a defect is found. What we would like to advocate here is a way to model your software development process, being aware of the fact that during the last 20 years many techniques arose to reduce or even erase the need for debugging, making the development of software a one-way process, from customer requirements to implementation with few or no features bouncing between being done or in progress.
5.3. You Don't Know What You've Got 'til It's Gone
What's the value brought in by a complete unit and functional test suite covering the most hidden spot of our application and the most complex synergy between two classes? While we could be tempted to see those tests as just a bug-ratio reduction tool, there's a lot more in them. Analyzing the way a web software project typically makes people closely relate to each other and the way bugs affect those relationships, we will be able to see much more value in using automated tests. The following sections will show you how automated tests can bring in value that goes beyond strictly technical issues, addressing many typical concerns often overlooked by usual project management by strongly empowering the team to be successful.
5.3.1. Trust Me: Communication
Among the ways automated tests address chaos is that they provide better communication among team members and between the team and the customer. Software is hard to keep tidy. It is such a chaotic beast that we cannot even be sure we understand the customer's requirements until the day we deliver it, unless we use some unambiguous way to agree on what has to be done. In the manufacturing industry, each product is described by a well-defined list of tests to distinguish acceptable products from bad ones. Automated software tests represent the same type of constraint: unit tests state how the system is supposed to do its job, while functional tests state what the system is expected to do.
Functional tests are a perfect tool to supplement or even replace traditional customer requirements documents. Every detail emerging from conversations among the customer, the interaction designers, and the development team should be frozen into some functional test, not to be forgotten, overlooked, misunderstood, or left untested.
Unit tests then define how those detailed requirements become working software. Every requirement is not considered fulfilled until every design detail of its implementation is well covered by a proper set of unit tests. This way unit tests become a conversation place for developers to agree upon the structure of the software they are all committed to, improving the spread of implicit knowledge about the project.
If you are new to automated tests and used to traditional documenting processes, you may think that good old ways to write documentation always worked, and so learning a brand-new way would be just a waste of time. We won't argue about the value of well-written documentation—though we could argue about the effort required for keeping it up to date. What we strongly oppose, though, is thinking there's some real alternative to writing tests to find out whether your software is doing what it's supposed to do. Writing those tests before the implementation of the working code would be a way to get even more value from your test suite, enabling their use as documentation.
5.3.2. Listen to What You Say, and Write It Down
Every time a programmer implements a feature requested by the customer he looks for a quick way to check when it's done, so he can start working on the next task. This is how code is developed by everyone, everywhere, anytime; it couldn't be different. Whenever we do something to reach a planned goal we have to find a way to know when the goal is reached, no matter the issue with which we are coping. That said, once we have found a way to test the effectiveness of our work, why not capture that test and reuse it freely?
The best thing about automated tests is that they are cheap, they are coded by developers (the only ones to know which details deserve to be tested first and how), and they can be run at will. This causes teams using automated tests to run them as often as possible since they provide a complete and objective status about the quality of their code. No one we introduced to automated tests ever went back, not once in years. This is because we always need something to report our progress instantaneously.
Feedback is also an issue in the customer-developer relationship: the customer has the right to understand and be sure that everything he asked for is going to be delivered, and he also has the right to information that is as close to real time as possible. The money invested by the customer in development deserves our respect, and letting him know how well the investment is going is a minimum requirement. Functional tests are the tool to give the customer almost real-time feedback about the project's progress rate. As far as every requirement is expressed in a functional test, the more functional tests pass, the closer we are to the end of the project.
The most important strategic team commitment about testing should be to keep them quick. Non-automated tests or tests taking too long will be run with less frequency. This will naturally tend to cause more new functionalities to be tested at once, to reduce testing overhead. This strategy will bring failure more often on a larger code changeset, making it harder to spot the problem, and raising the bug rate again.
5.3.3. Pleasant Constraints
Refactoring is a technique that involves changing critically large portions of already-working code. This can be very dangerous, because of the unintended consequences and side effects. Thousands of details spread across hundreds of thousands of lines of code must all be correct, with hundreds of components communicating with each other in a hard-to-predict way. As the codebase grows larger, we are exposed to an exponentially larger risk.
Even if you are not using refactoring, you should be aware that usually in the software industry half of the development occurs after the initial release. That means that the software needs to be safely changed anyway and that those changes must be cheap. Many common techniques make changes safer: encapsulation, layering, external components, web services, and open sourcing. But the most effective way to ease changes preventing disasters is an automated test suite that tests the code the developers intend to change and the system behavior the customer is going to get in return.
An up-to-date test suite will not only snipe emerging bugs, keeping the system close to its quality requirements, but will also greatly reduce the scope to look within to fix the bug. The developer's duty then becomes to write those tests, freezing requirements in a formal language, making sure they are correct, complete, version-controlled, maintained, and considered part of the released product. Even if they end up constituting a large portion of the overall codebase, they will be many times worth the effort they will require to be created and maintained.
As more functional tests add up with time, they become an ever-growing repository of requirements that will never be forgotten, not even once through the whole system lifespan. This is great news for development and testing teams that have to run through the whole system every time a change is made, in order to be sure no regression has been introduced by the change itself. This is great news for the customer, too, because quality assurance teams can be very skilled and proficient, but they are human—sooner or later they will be wrong. Automated tests don't get distracted, bored, annoyed, angry, or tired. They just perform their duty.
5.3.4. Create Trust
Communication, feedback, and safety nets all together deliver a fourth crucial value in the life of a team: trust. Communication is the root of trust. Proven working software builds the customer's trust in the team. An unambiguous design specification increases the team members' trust in each other. A widely applied test suite may communicate many times a day how good the team is performing, improving the manager's trust in the team. A software project in which trust is not increasing day by day, minute by minute, is a dead project. Defensive behaviors will start creeping in and people will begin to cheat. There is no way to squeeze value out of a cheating team, since defensive behavior will not create business value—just like debugging won't.
A winning project is one devoted to building value, not one that incentivizes barriers. Any tool, technique, or mindset capable of raising trust among the whole team to the highest level is always worth its cost. Automated tests are one of those tools.
5.3.5. Test-Driven Development
At least a brief acknowledgment should be given to a discipline based on a test-first programming approach, unit tests, and refactoring: test-driven development (TDD).
This set of techniques is a way to discover the best design for our systems in an amazingly effective way to combine top-down and bottom-up thinking. On one hand, it empowers developers to think of the main tasks and responsibilities without worrying about the interactions going on at lower levels; on the other hand it requires us to develop the simplest actors and interactions that solve our problem. The main mantra of test-driven development is Red, Green, Refactor. What does it all mean?
Red: We write a failing test expressing in a well-defined way what we want our unit of code to do.
Green: We write the minimal amount of code needed to make the test pass.
Refactor: We refactor the code we wrote to improve its design without breaking tests. In case we need to create new tests and new units of code to accommodate the newly improved design, we apply TDD to those components, too.
While we consider this theme to be too crucial to be reductively explained in a single chapter of this book, it is also true that very few written resources exist on this subject. This is mostly due to the inherently non-theoretic nature of this discipline, making it very hard to define and explain it by means of a book. No wonder Kent Beck's main book on TDD [TDD] is based on a series of examples to make TDD clear to the reader.
TDD, by the way, represents the state-of-the-art use of automated tests as a tool for not only checking and anticipating the correctness of code, but also guiding its design.
----Example Code------------
Doing this refactoring is as easy as writing the following code.
class Person
{
public $firstname;
public $lastname;
public function getCompleteName()
{
return $this->firstname.' '.$this->lastname;
}
}
---------------------------
7.7. Replace Method with Method Object
Problem:
"You have a long method and you can't apply 'Extract Method'."
Solution:
"Create a new class, instantiate an object, and create a method. Then refactor it."
7.7.1. Motivation
Sometimes local temps make method decomposition too hard, forcing us to keep very long methods. Since we all love writing good short methods (don't we?), we can perform "Replace Method with Method Object" and turn all those local temps into fields on the method object, and then refactor this new object to decompose the original behavior.
7.7.2. Mechanics
-
Create a new class named as the method.
-
Give the new class an attribute for the original object and for each temp or parameter in the source method.
-
Write a constructor to accept source object and parameters.
-
Create a compute() method in the new class.
-
Copy the body of the old method into the new one, using the object field in every invocation of original object methods and adding $this-> prefix in temps and parameters references.
-
Replace the old method with the object instance creation and a compute() method call.
-
Decompose compute() as much as needed considering that all temps are now object fields.
7.7.3. Example
Consider this not-so-meaningful class:
class Account
{
private $state = 3;
public function getTripleState()
{
return $this->state * 3;
}
public function foo($first_param, $second_param)
{
$temporary_1 = $first_param * $second_param;
$temporary_2 = $first_param * $this->getTripleState();
if (($temporary_1 - $temporary2) % 2)
{
return $first_param - 2;
}
return $second_param + 4;
}
}
Consider also this associated test:
class AccountTest extends PHPUnit_Framework_TestCase
{
public function testFoo()
{
$employee = new Account();
$this->assertEquals(24, $employee->foo(10, 20));
$this->assertEquals(9, $employee->foo(11, 17));
}
}
We apply the first steps of "Replace Method with Method Object," and we get the following class:
class foo
{
private
$account,
$temporary_1,
$temporary_2,
$first_param,
$second_param;
public function __construct($account, $first_param, $second_param)
{
$this->account = $account;
$this->first_param = $first_param;
$this->second_param = $second_param;
}
}
We can now move the method from the source object to the new one.
class foo
{
[...]
public function compute()
{
$this->temporary_1 = $this->first_param * $this->second_param;
$this->temporary_2 = $this->first_param * $this->account->getTripleState();
if (($this->temporary_1 - $this->temporary2) % 2)
{
return $this->first_param - 2;
}
return $this->second_param + 4;
}
}
In the source class we just replace the old method body like this:
public function foo($first_param, $second_param)
{
$foo = new foo($this, $first_param, $second_param);
return $foo->compute();
}
That's all. The best has yet to come, though, because at this point we can start extracting methods without bothering about arguments, like in the example here:
Code View: Scroll / Show All
public function compute()
{
$this->setup();
if (($this->temporary_1 - $this->temporary2) % 2)
{
return $this->first_param - 2;
}
return $this->second_param + 4;
}
public function setup()
{
$this->temporary_1 = $this->first_param * $this->second_param;
$this->temporary_2 = $this->first_param * $this->account->getTripleState();
}
These are notes I made after reading this book. See more book notes
Just to let you know, this page was last updated Thursday, Nov 21 24