Refactoring code is time invested into quality, a process
that makes the lives of developers easier and their work more effective. It is a part of the development process to
repay the technical debt inevitably accumulated while adding functionalities or
patching problems and we know very well how bad things can happen when
refactoring is neglected for too long. Modern
managers understand the importance of refactoring and how it affects the
monetary cost of software maintenance and defect rate.
We all love refactoring. Yet I believe not to be the only developer who
experienced how sometimes refactoring can go horribly wrong and wished to have
done things differently. Also, the time
available for refactoring is realistically limited and precious; hence, it is
utterly important to achieve the maximum benefit from it. This is why it is the responsibility of the
technical leadership to question and positively criticize the refactoring plans
and activities of the development team to ensure that the efforts are spent in the
right direction. Looking back at my career, I can now pinpoint some major root causes of failure when undertaking code refactoring:
Lack of Goals & Planning
Refactoring could be an immense waste of time by chasing
some kind of aesthetic perfection that has no impact and little meaning in our
applications. The fact that refactoring
does not add nor change any functionality does not mean that the end results
are not measurable. Refactoring is
effective and always produces very tangible results if we have a clear
understanding of the goal that we want to achieve:
What is the problem
that we want to solve through refactoring?
What are the tangible advantages?
What are the risks? How can we minimize them?
These are key questions in order to determine the goals of
Here are some examples of goals:
The Code More Robust To Changes
The code is too flaky. Every time we need to make a change it feels
like touching a castle of cards. Something
always breaks, we fix a problem and in the process we create two more,
sometimes not related at all to the one we fixed. Customers constantly complain about new
defects on things that were working just fine. This is a typical issue for applications with
intricate interdependencies and high coupling.
It is hard to accommodate new requirements.
New functionalities always present too
many challenges and need to be carefully planned. Customers complain about the long and costly
estimates. This is a typical issue for
applications that grew too quickly or that were tailored only upon very
specific requests, without looking at the bigger picture.
Our applications do not send rockets to Mars, yet sometimes they are
designed as if they would. Sure, the
architect had his fun overdesigning multiple layers of abstraction to take into
account impossible scenarios and way too many “what if
”. Now is time to go
back to reality and use common sense. The
learning curve for the application is way too long and simple maintenance
becomes unexplainably complicated and expensive in order to support all the
unnecessary layers, wrappers, interfaces, deep inheritance chains, and so on.
Obscure code can be there for many reasons:
Inadequate clarity of mind, lack of care for the difficulties of others in
reading and understanding code, or perhaps a distorted conception of job
security. Whatever the reason is, it is
too hard to understand what is going on in the code, names are misleading or
uncommunicative, strange things are done in the code without any apparent
reason or explanation. No comments,
diagrams or documentation are available. Nobody can confidently change the code and the
application is nearly immutable.
Tracing or error logs are either not
available or they are not detailed and meaningful enough to help us in figuring
out what is wrong and where. When a
customer reports a malfunction, the investigation takes an awful amount of time
and it is frequently requires the direct involvement of a developer. Reproducing bugs is hard and it frequently
requires the total replication of the customer’s environment (data and
software) in a local network where it can be more easily monitored and debugged.
Every time the code is deployed something usually fails. Failures are different every time. Too many deployment manual steps, too many
things to remember, too much configuration needed. If too many things can possibly go wrong,
something usually does go wrong. These
are the cases where we need to add conventions to the code, default behaviors, add
guard classes to diagnostic configuration issues, etc.
The application in time has acquired more users, more data, and more functionality.
While the code initially had acceptable
performances, now it is getting slower and something needs to be done before customers
start complaining. This is an edge case (excessive
slowness may be rightfully considered a bug). Before refactoring, we need to profile the
code and identify the real factors of the performance degradation that need to
be corrected. Guessing what needs to be
improved without any assessment can be a terrible mistake.
Once the goals are clear, then we also need to plan. Depending on the problems identified in the code,
the medicine may be very different. We
need to identify the scope of the changes, plan the refactoring in small phases
and make sure that among the development team everybody is on the same page about
which strategy to use.
Without tests, refactoring can be described as the
transformation of perfectly working production code into code that most likely
will have several bugs and will not work as expected. By definition, refactoring means to make improvements
through non-functional changes. Yet, if we
add defects, the changes that we make are indeed very functional (even if not
intended), since they do affect functionality by breaking it.
That is why refactoring without tests is not refactoring: It is just breaking code with very slim
chances to find out issues until is too late. The longer and more critical are the changes,
the bigger is the risk.
If the code to refactor has few or no unit tests, then the
first step should be to write them, at least enough to guarantee a decent
coverage before we start changing stuff around. This will slow us down since not only do we
have to write tests, but we may also have to change them along with the
refactored code. Nevertheless, the effort
will pay us back with interests by shortening the go-live time, minimizing
defects, and avoiding the bad reputation derived by breaking working code.
In the unlucky scenario where the code to refactor is so
poorly engineered or so obscure to be virtually untestable, then a viable
approach is to build a safety net of high level functional tests to have a
measure of comparison of the code before and after the changes (see reference GTAC 2010 for more info on this topic).
Chewing More That We Can Swallow
To save time, we may be tempted to undertake the refactoring
of a great amount of code in one shot. This
usually ends badly: while we are
refactoring, requests for new functionalities may occur and we would have to do
them in two places. Problems will arise
and we will not be able to go-live with the new code for a long time and will end
up going back to maintain the old one. While
production code is under refactoring, it is exposed to new requests, patches, and
old and new bug fixes. It is wise to
limit the exposure and the risks by breaking the refactoring into small stable
steps, even if the overall effort will be bigger.
Poor Knowledge of the Code
Some refactoring activities do not necessarily require a
deep knowledge of the code (e.g., refactoring for testability), while some
other activities (e.g., refactoring for performance) may require a high level
of intimacy with the business logic and processes of an application. Knowing the difference is important, since
lack of experience may lead to naïve errors and bind the project to failure. If the refactoring is done in critical
components or in the guts of complex business logic, then the most experienced
developers should take the lead and responsibilities instead of delegating the
tasks to interns or junior developers who previously spent only little or no
time at all with the code to be refactored.
Neglecting the Business Side
At long last, we did it! We successfully refactored the code achieving
all of our goals. Well, hold on to that
bottle of champagne, since we are not done yet. We can see the tangible effects of our
refactoring strategy. We now need to
make sure that also the business sees these results. Somebody paid for all this refactoring time—next
time they may not be so generous. We
should not go to business managers or CEOs bragging about “geeky” achievements such
as, “Loose Coupling” or “Low Cyclomatic Complexity.” Chances are that they will not understand a
word of what we are saying or worse, they will think that we are totally insane
and that they just wasted time and money in a useless project. Instead, we need to talk to them in the
universally understood language of business: Money. We can show them how the defect rate dropped,
saving hundreds of hours of bug fixing. We
shall emphasize how much faster we were able to add new features to our program
and deliver everything on time. We need
to communicate the tangible results in a business-tangible way. We accomplished difficult and risky goals
because we like what we are doing and care about the quality of our work. Let’s make sure that the right people know
about it and the next time we ask for extra time to refactor code, we will have
a more favorable leverage.
Esteban Manchado Velazquez
GTAC 2010: Lessons Learned from
Refactoring - Improving the Design of