Share via


When Refactoring Is A Bad Idea

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 at least some of the root causes of failure when undertaking code refactoring:

 


1. 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 refactoring.

Examples

Here are some examples of goals:

Make 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.

Increase Flexibility.

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.

Simplicity.

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.

Clarity.

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.

Add More Introspection.

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.

Reduce Deployment Failures.

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.

Boost Performances.

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.

 


2. No Tests

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).

 


3. 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.

 


4. 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.

 


5. 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.

 


References

Wikipedia