Engineering

Refactoring Part 1 – Master the Well-Gardened Code

Share article

Legacy Code

Everyone must leave something behind when he dies, my grandfather said. A child or a book or a painting or a house or a wall built or a pair of shoes made. Or a garden planted. Something your hand touched some way so your soul has somewhere to go when you die, and when people look at that tree or that flower you planted, you’re there.

— Fahrenheit 451, Ray Bradbury

Isn’t that an excellent description of the word legacy? It would be a shame to associate such a lovely word with something negative, wouldn’t it? Well, not according to developers. Add one more word at the end, and we get the shortest horror story – Legacy code.

But why does this phrase often carry a negative connotation? Why are there projects we feel nervous and hesitant to open? It’s like becoming the main character in a scary movie, with our colleagues watching and whispering at us, “Don’t go in there.”

Do our customers play any role in it, and are they the reason software systems almost always descend into chaos despite the best-laid plans and a skilled team? After all, they are always looking for ways to improve and modify our perfectly written code and change the requirements. Who would have thought that software would require modifications once it is delivered?!

Walking on water and developing software from a specification are easy if both are frozen.

— Edward V. Berard

Ok, Edward, so we should foresee it. When we modify software, its complexity will increase unless we actively work against it. Our aim as software developers is to create designs that tolerate change. Unlike hardware, software lacks any physical components, which should allow flexibility and adaptability. As our thoughts and understanding evolve, we should be able to change the code to reflect our ideas easily.

The broken windows

Sometimes, what we label as bad code might have been considered reasonable in the past, but at some stage of the project lifecycle, we might have taken a wrong turn.

What can make the difference? A good glassmaker.

You might be familiar with the broken window theory by James Q. Wilson and George L. Kelling published in 1982. The experiment that illustrates the theory was conducted in 1969 by Stanford psychologist Philip Zimbardo. By leaving cars without license plates in two different cities, with the only difference being that the car in Bronx had its hood up, Zimbardo observed that prompt vandalism occurred in the Bronx. The car in Palo Alto remained untouched until the experimenter initiated the damage, leading to rapid destruction by seemingly respectable individuals in both locations. A sense of abandonment led to much bigger problems in a relatively short period.

Moreover, by leaving the car or apartment window broken, nasty little bugs can get in. Those same bugs will inhabit our code base if we don’t handle it. Bad designs, wrong decisions, or poor code lead to severe issues. So, as every Pragmatic programmer knows – don’t leave broken windows unrepaired – be a glassmaker.

Taking shortcuts

Technical debt is the cost of opting for quick solutions over effective ones. The phrase, originally coined by software developer Ward Cunningham, is often misunderstood as solely negative.

It’s crucial to recognize that if technical debt is managed effectively, much like financial debt, it can be a good thing. However, while it may offer short-term benefits, failing to address it accumulates interest over time, making future enhancements and fixes increasingly challenging. A development team must understand the balance between quick decisions and efficient rework.

Another thing to understand is that, when a loan shark comes to collect the debt and says, ‘Hop in the trunk, we’re going places,’ the destination is probably not a pleasant road trip.

Tests

Ignoring testing can contribute to the growth of technical debt. Without adequate testing, it becomes harder to identify and fix issues promptly, leading to increased maintenance costs and potential compromises in the software’s functionality.

As we know, there are various types of testing in software development, and each serves a specific purpose in ensuring the quality and reliability of a software application.

If you like debating, ask a group of developers what percentage of which type of tests to write. Is following the traditional Testing Pyramid approach more fitting, emphasizing the importance of unit tests? Or should we lean towards the Testing Trophy concept, prioritizing the integration tests? If we have microservices, would the more suitable approach be to structure our tests according to the Testing Honeycomb

Whatever the results, remember what Jim Jeffries once said – We’re not animals! We live in a society! Write tests for people!” That last part is by Gerard Meszaros, but it fits nicely.

Effective tests serve as documentation for the production code. For each usage scenario, the tests should:

  • Explain the context, conditions, and prerequisites that need to be met and specify the starting point
  • Demonstrate how to invoke the software
  • Outline the expected results or postconditions that require verification.

Tests are also crucial because they provide valuable feedback when making changes in the code.

Refactoring

In its essence, writing the code is articulating an idea. Ideas are dynamic – their comprehension evolves with time and experience. At some point, we will need to reconsider previous decisions and revise code segments. This process is natural, and we should embrace the fact that we always work with uncertainty and incomplete knowledge.

Rewriting, reworking, and re-architecting code is collectively known as restructuring. But there’s a subset of that activity that has become practiced as refactoring.

The Pragmatic Programmer, Andy Hunt and Dave Thomas

Martin Fowler defines Refactoring as a change made to the internal structure of software to make it easier to understand and cheaper to modify without changing its observable behaviour. Additionally, when referring to Refactoring as a verb, Martin uses the following definition – to restructure software by applying a series of refactorings without changing its observable behavior.

Refactoring is all about making minimal, semantics-preserving changes – it’s a modification that doesn’t change the observable behavior of the program. When developing software, the idea is that we move back and forth between adding new features or refactoring. Kent Beck came up with a metaphor of the two hats. He says there are two kinds of changes — behavior and structure changes. Behavior changes include adding functionality. On the other hand, when we are refactoring, we are only restructuring the code. We should never make both kinds of changes simultaneously, much like we wouldn’t wear two hats at once.

Why should we refactor?

If we focus solely on our problems, we’ll have more problems. When we modify the code to fix some bug or quickly add new functionality, we might be degrading the structure and design of our code base. Regular refactoring is vital for keeping and improving the design of our software. Moreover, we are trying to make the program work at the first stage of development.

Once we achieve that, we must focus on the developers coming after us. We can follow Martin Fowler’s statement: “Any fool can write code that a computer can understand. Good programmers write code that humans can understand.” After refactoring, we should have code that works and is well structured. Also, regular refactoring increases our understanding of the code and the internal design, enabling us to detect bugs quickly and work faster in the long run.

Performance and refactoring

Does refactoring impact the performance? Usually not. In fact, it may uncover opportunities for performance enhancements. Typically, only a small portion of code significantly influences a program’s performance. However, we cannot look at a block of code and judge whether it will be fast or slow. We’ll have to do performance testing and find the hot spots to gain the correct insights. We don’t guess. We measure with the proper tools, like profilers.

When should we refactor?

The best time to start refactoring will be thirty minutes before sunrise on the 27th of January when Mars, Venus, and Mercury align in the morning sky. Once Mercury is a few degrees lower than Venus, we should stop all activities.

Sounds right? No! We shouldn’t wait for the planets to align before we start refactoring. Refactoring is a daily activity (same as personal hygiene).

We should refactor when we learn something new. When we spend some time analyzing the code, our understanding gets better. At that point, we have that understanding in our mind. And, as Ward Cunningham phrases it, it is time to move it from our head to the code itself. By doing that, we preserve it and make it visible to others.

There are also times when we want to make a change, and it turns out that it is easier to change the code first and then put the new functionality, or, as Kent Beck would say;

For each desired change, make the change easy (warning: this may be hard), then make the easy change.

Indicators – Code smells

Code smells are common issues in code quality. They often indicate a deeper problem within the system and a good sign our code needs refactoring.

Detecting and resolving code smells helps make focused improvements to enhance overall code quality. However, it’s worth noting that not all code smells necessarily require immediate action, and we should consider the specific context and priorities of the project to decide whether to refactor or postpone that activity.

It’s relatively easy to spot them if we know what we seek.

Let’s name a few examples:

Duplicated code
Duplication is problematic because it bloats the code, making it more challenging to maintain. We must find and address code duplication to simplify the structure and maintain a clean and efficient codebase.

Long functions
Keep an eye out for really long functions – it’s usually a sign of trouble and correlates with code complexity metrics. The longer a function is, the more difficult it is to understand, test and modify.

Mysterious name
Naming in software is everything. It is hard to call things correctly, but we must do it with precision and care.

Let’s have a little test. See how long it takes to figure out which word is written in its own color.

The answer – the word GRAY.

In psychology, this is known as a Stroop test. It shows us that, when faced with two stimuli at the same time, we give preference to the written word. This means that our brain reads words faster than it recognizes colors. Talk about the importance of a good naming! Unsurprisingly, the most common refactoring operations relate to renaming (variables, methods, classes).

Mutable data
Data mutability brings complexity, and it often leads to unintended side effects. Immutable objects require no synchronization (thread-safe) and can be shared freely. They make great building blocks for constructing other objects, regardless of whether those objects are mutable or not. We should strive for immutability throughout the codebase.

Speculative Generality
Speculative Generality occurs when we craft code with excessive caution for possible future changes. We anticipate new features and scenarios, adding hooks and Justin Cases in our code. Doing that makes it unnecessarily complex and hard to understand and maintain. Striking the right balance when anticipating changes can be challenging, but we should only add functionality once necessary.

Comments
Cory House said, “Code is like humour. When you have to explain it, it’s bad”.

Whenever we feel the need to write a comment to explain a block of code, we should first try to refactor the code and express our ideas that way. Although the compilers ignore comments, they are not invisible to other developers. When comments are correct and up to date, they can be helpful. However, the probability of a comment being incorrect increases with age, primarily due to the challenge of maintaining both the code and associated comments.

Still, there are good types of comments, and one is particularly necessary if we are writing a public API. Meet the Javadocs, a standard for documenting APIs.

For an API to be usable, it needs documentation. As Joshua Bloch mentions in his book Effective Java, proper documentation involves adding doc comments before each exported class, interface, constructor, method, and field declaration. The Javadocs should concisely describe the contract between the API and its user. For the same reasons mentioned above, Javadocs can also be(come) deceptive, outdated, and incorrect, so we should take special care of it.

How to refactor?

Refactoring is not just a shortcut key. The first version of Martin Fowler’s and Kent Beck’s book about refactoring – Refactoring: Improving the Design of Existing Code – was published a few years before refactoring tools became widely available. Refactoring has a more extended history rooted in design and design thinking.

Nevertheless, we should use automatic tools to perform refactoring operations like renaming variables and methods, extracting a long method into smaller ones, inlining variables, etc. Our IDEs have tests written for each refactoring operation they offer.

As mentioned earlier, we should also have tests before changing our code. Refactoring is a disciplined activity. We must always modify the programs in small, traceable steps. If we make a mistake, it is easy to spot it. Another good habit is doing micro-commits. With micro-commits, we focus on a single task or fix a specific issue, which is very effective and productive. It also enables us to select the particular point in (git) history we want to return to – like we are time travelers.

A nice add-on to the beautiful book about refactoring by Martin Fowler and Kent Beck is Catalog of Refactorings, which offers practical guidance on making effective changes to the code and implementing best practices.

Final words

The key to successful refactoring is consistent practice. Work on it daily in your projects and with your team. While it might initially seem intimidating, remember to take small steps and commit frequently. It is easy to roll back to a previous commit if anything goes wrong. This approach makes refactoring a low-stress task, thanks to version control systems. Since the teams usually consist of developers with different skill levels, it’s also important to remember that showing people how to fix things rather than just fixing them ourselves is crucial for building a collaborative and learning-oriented environment.

And, as with all things in life, what we do today will shape our tomorrow. We can craft elegant solutions and introduce innovations. From time to time, it’s good to take a step and reflect. What decisions will define our journey and what will be our legacy?

If you want to learn more about how you can get there, take a look at our other blog posts.