C H A P T E R  4

image

Principles and Rules

Refactoring software is a practice adopted during recent years within agile software development processes. Like all agile practices, refactoring is based on clear principles and rules that must be observed to improve the software production process.

In this chapter we'll see the main principles and rules about software refactoring. We'll see why and when we should do refactoring and also why sometimes we shouldn't do it.

Why Should You Do Refactoring?

There are four important reasons for introducing the refactoring practice in your production process:

  • Refactoring improves the design of our software.
  • Refactoring makes our software easier.
  • Refactoring helps find bugs.
  • Refactoring makes the team more productive.

All four reasons are really important in software development. If your team is already very good at producing software with good design, which is simple and free from bugs—and all of this at a satisfactory production rhythm—then refactoring just might improve this excellent process, assuming that your team can always do better.

Refactoring Improves the Design of Our Software

In the last few years software production has changed a lot. The needs are growing, domains are very disparate, customer requirements are often unclear, software must change very quickly, and deadlines are very narrow. With these assumptions it is a struggle for even the best software architects to create a good application design that is always correct throughout the software's life cycle. What we did yesterday may no longer reflect today's needs, or what we knew yesterday may be different than what we understand today. Grasping complex domains is a difficult task for developers, and design software in these domains is difficult too.

Through refactoring, with many small steps, we can change, not the functionalities of our software, but the design of how these features have been implemented, without losing the value already accrued. Day after day we can improve the design of our software, while the software grows; we can bring out the correct design in time, rather than trying to force an early design of the project. Our software will fit like a glove.

Thanks to this ability to embrace change, everything that is technically feasible will be possible. We will never be afraid to change direction; we will always be sure of keeping the produced value.

For example, if we have a Person object that can have only one address, but we want to change its behavior, adding more than one, we can improve its design, first extracting the address class as in Figure 4-1, and then implementing the ability to add other addresses.

image

Figure 4-1. Extract class

Refactoring Makes Software Easier to Understand

How many times have you ever wanted to change software after months without working on it, and realized that you forgot everything and no longer know where to start? How often have you been forced to change code written by others, while being afraid to add a single line of code, thinking that even the slightest change could break everything?

When you are faced with code that is hard to read, called “spaghetti code” because it is as tangled as a plate of spaghetti, the desire to throw it away and rewrite it from scratch is always great. There is software that has been unchanged for years and cannot grow because there is no one developer who can modify it, except the one who created it.

The technique of refactoring helps us make software easy to understand, so any developer can get back to working on code after months, without needing to remember everything or being afraid of breaking something by changing only a small line of code. This is because all the methods applied in refactoring are based on the principles of “keep it simple and stupid” (KISS), “don't repeat yourself” (DRY), and “test-driven development” (TDD).

For example, by changing our application from procedural code to object-oriented code through the techniques of big refactoring, we make our code easier to understand and modify, because, in general, reading an object-oriented code with a good design is simpler than reading a functional code.

In general, bad smells make our code difficult, but by removing them we make our code easier.

Keep It Simple and Stupid (KISS)

“Everything should be made as simple as possible, but no simpler.”

—Albert Einstein

“Keep it simple and stupid” refers to keeping our code as simple as possible. It doesn't mean “easy,” but as “lean” as possible. We have to think only about what we need today and not try to foresee the demands that might come in the future, since, after all, the human mind is not capable of predicting the future. Simple means doing something as well as possible, but in the simplest way.

Don't Repeat Yourself

“Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.”

—Andrew Hunt [HUN99]

A duplicate code is difficult to read and maintain, as a code that does the same thing in the same way in many parts of our software, or does the same things but differently. Duplication of code lines or functionalities is the biggest flaw in our software, because we can't change it simply and it could exponentially increase the risk of introducing bugs. Less is better. Our code shouldn't be duplicated, and we must not repeat. Our system must implement functionality in only one point. Only in this way we can maintain and modify our software easily.

Test-Driven Development (TDD)

Test-driven development is a software development technique that consists in the repetition of a short development cycle divided into the following steps:

  1. Add a test.
  2. Run all tests and see if the new one fails.
  3. Write the simplest code that will cause the test to pass.
  4. Run tests and see them succeed.
  5. Refactor the code.

Once the cycle is finished, you can start again with a new test (TDD). A study found that TDD drives developers to write more tests, debugging less and making a better design for the software they are working on.

Testing software in a unitary and automatic manner means being able to measure change. Unit tests are to software development what standard units of measurement are to physics. Without the ability to measure change, physicists would not be able to perform their experiments. Without unit tests, software engineers would not be able to modify software and discover whether something has changed.

The practice of refactoring is based on TDD, because it is a subset. There can't be refactoring without TDD, and TDD cannot exist without refactoring.

Refactoring Helps You Find Bugs

Finding bugs before they go into production is truly an art. There are excellent programmers who can read hundreds of lines of procedural or object-oriented code and instantly find all the bugs. Unfortunately, not all developers have this skill. For me, finding a bug from hundreds of lines of code is like finding a needle in a haystack.

When we do refactoring, our work is low-level, so we must understand what a piece of code does in order to decide whether we have to change it. In this activity, finding bugs becomes very simple, because when we understand what our code really does, we understand also what the code doesn't do. Repeating this activity every day in every single piece of code greatly increases the chances of finding bugs before they get into production.

When we write tests for our software, we have to understand what the code does to be able to test everything that could easily fail. Thanks to this detailed work, we can find a lot of hidden bugs.

Refactoring Increases Our Productivity

We have seen that refactoring improves the design of our software, as well as its readability, retention, and quality, by reducing the number of bugs. But what can we say about the development speed?

Productivity, in software development, is a measure of how much functioning, corrected, and tested code a developer produces in a unit of time.

It might seem that refactoring slows the production of software, focusing our efforts too much on quality and not enough on delivery dates, a bit as if we had unlimited time. Actually this perception isn't true. When we begin to write code for a new software, at first, may seem faster to write procedural code rather than object-oriented code, to provide a prototype or to meet delivery, but what happens when a customer ask us to change what we did because it doesn't fully meet the requirements? What happens when the customer finds the first bug and we need to fix it? What happens if the customer asks us to add a new feature? After the first delivery, the time we spend in modifying the software, finding and resolving bugs, and adding patches to meet the customer requests becomes uncontrolled and immeasurable, as we can see in Figure 4-2.

image

Figure 4-2. Classical cost of change

We can change this strange habit. By giving our software the right quality, creating an appropriate design, decreasing the presence of bugs, and testing our code, we'll pay a small initial price, but then the curve of maintaining complexity will be linear and measurable, as we can see in Figure 4-3. We will never lose the value acquired, and we won't be afraid to embrace change, when needed.

image

Figure 4-3. Cost of change with refactoring

When Should We Do Refactoring?

When we should do refactoring is a question that developers ask me often. My answer is very simple: “When tests are green.” Our primary goal is to provide immediate value to our customers and stakeholders, and our second goal is to provide this value in the best way. It's in the second goal that we can do refactoring. When a test is green, we have given value to the customer. If we realize that we can improve the code, we have to do refactoring.

This rule is very simple, but first, we have to be familiar with the refactoring techniques. To achieve this understanding, we can start with three simple and explicit rules.

The Rule of Three

This rule is the one I enjoy most. It was presented for the first time by Martin Fowler in his book Refactoring: Improving the Design of Existing Code[FOW01].

As we have seen, one of the worst enemies of clean code is code duplication. The first rule seeks to mitigate the copy-and-paste action. It says that if there are problems in our software that are solved with the same piece of code or portions of such codes, we can duplicate this piece of code at most twice. The third time, however, we have to do refactoring and remove the duplication.

By applying this simple rule, you'll see hundreds of lines disappear from your software and you'll,begin to understand the behavior and communication of your objects, if you didn't before. Try it now.

For example, we have two classes, Student and Teacher, that extend the Person class:

class Student extends Person
{
  ...

  public function __toString()
  {
    return ucfirst($this->lastname).' '.ucfirst($this->firstname);
  }
  ...
}

class Teacher extends Person
{
  ...
  public function __toString()
  {
    return ucfirst($this->lastname).' '.ucfirst($this->firstname);
  }
  ...
}

If we need to add a new class called Manager that extends the Person class, and implement the same toString() method, we can't duplicate the method, but we have to refactor it—for example, by moving the method in the super class and removing it from subclasses.

class Person
{
  ...
  public function __toString()
  {
    return ucfirst($this->lastname).' '.ucfirst($this->firstname);
  }
  ...
}

Refactoring When You Add Functionality

Another magic moment—or tragic, depending on the code with which we are working—is when we add functionalities to our software. Regardless of whether the software has a perfect design, when we add functionality, especially if the code wasn't written by us, we must first understand what the software does. If we realize that the software design isn't appropriate to accommodate new features, we have to do refactoring to change and improve it to receive the long-awaited new features. Through this practice, the design of our code evolves constantly, perfectly incorporating the proper behavior of the implemented features.

For example, as we saw in Figure 4-1, we did refactoring of our code before adding a new functionality.

Refactoring When You Need to Fix a Bug

When the customer or a beta tester reports a bug, our job is to fix it. To accomplish this arduous task, the first thing we do is find out what our software does during the process in which the bug happens. Before starting to debug, it's advisable to try to reproduce the bug locally. Write a test that fails, and once you have the red test, begin the inspection. Once we fix the bug, we can be sure it will not recur, because next time the test will notify us of the error before going into production.

If during debugging tasks we find that the bug is due to a wrong block of code or a wrong design, first we have to fix the bug in the easiest way. Then, when the test is green, we can start doing refactoring.

In most cases, the proliferation of bugs is due to an untested code, which is code with an incorrect design. If you cannot test your code, don't blame the testing framework. Rather, ask whether your design is correct.

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.

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.

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.

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:

  1. Find the piece of code to change.
  2. Write a unit test if you didn't.
  3. Do a small step to improve.
  4. Run the test and fix the code until the test is not green.
  5. 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.

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.

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.

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.

Summary

In this chapter we have seen the principles and rules behind the application of refactoring techniques. If we respect the principles and follow the rules, we can learn and perform all the techniques of refactoring in the best way.

In the next chapters, we deepen our knowledge of test-driven development and learn about some tools that can simplify the execution of refactoring techniques.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.222.117.4