How to Make the Best of your Code with Refactoring
In the early days of software, a system was thoroughly designed before you even put down the first line of code. Days went by making diagrams, outlining use cases, and charting estimates. By the time you started development, you had a long, meticulous blueprint of your system. You would feel, this time, you really got something special.
But as time passed, the design became stale. Soon, every piece of functionality meant an entirely new module with hundreds of lines of repeated code, and the code you wrote a month ago doesn’t make sense anymore. In short, the spark faded and you no longer loved your code like you used to.
Modern, agile software development paints a new picture of this relationship. Today, code design is flexible and constantly evolving and at the core of the process, we find refactoring.
What is refactoring?
In short, refactoring means restructuring existing code without changing the output. You may have heard the term “refactoring” used vaguely to refer to any modification of existing code. However, refactoring is actually a technique based on well-defined transformations that improve your code without affecting the user-facing behavior.
Refactoring helps us keep our code fresh, easy to maintain and understandable across the entire lifespan of the system. Clean design and architecture are no longer a fixed destination that needs to be achieved before development; instead, it’s a continuous journey.
One important consideration is that refactoring doesn’t intentionally fix bugs, alter any functionality or directly improve performance. The code is assumed to be in a working state before you start. In addition, the moment you change any observable behavior, you’ve stopped refactoring and entered the realm of debugging, feature development or performance optimization.
Put in layman’s terms, refactoring is similar to restating or paraphrasing a text. What I mean is, you use different words to convey the same meaning in a clearer way, like adjusting the phrase, “The students didn’t show any enthusiasm for the topic” to “The students showed apathy towards the subject.”
How does code refactoring benefit my team?
You may be wondering, then what’s the benefit of refactoring? Why would I spend any time at all changing code that won’t fix any bugs, add any functionality, or improve performance?
Often, refactoring is thought of as “perfectionism”, changing code purely in the name of “clean code”, to reach the current best practices or follow the newest, buzz-worthiest architecture.
The truth is, refactoring is much more than that, and we do it purposefully. In fact, it can optimize resources, save time and money, and help teams meet their goals, so think of it as an investment.
At my current project, even though our application is roughly a year old, refactoring is part of our culture. We continuously take the time to look back to the modules we use, maintain or need to extend, and enhance the design of the code when we find an opportunity. This doesn’t mean we disapprove of the way things were made or that we consider it “bad code”. It just means we constantly improve ourselves as engineers and find a great benefit in translating that improvement into the code we’ve written.
Before you start…
Refactoring doesn’t come without risk. After all, whenever we write code, we’re prone to introducing bugs or causing serious performance setbacks. Therefore, this is why refactoring cannot be done by someone without any understanding of the code.
The first step to safe refactoring is being aware of the subtle difference between refactoring and writing code that alters behavior. This doesn’t really mean that refactoring should be thought of as a “side project” that you split from your main sprint cycles; on the contrary, you can constantly improve the code as you go.
What this means is that, whenever you’re adding functionality and find code that needs improvement, you should take a clear break from the new functionality and focus on applying refactoring. Once you improve the code, then you go back to adding functionality. This mindset change can happen several times a day. Also, it’s supported by a version control system and frequent commit cycles.
Prepare your system for safe refactorings
Normally, you’d first want to have a comprehensive, well-written, trustworthy and automated test suite, based on behavior and decoupled from implementation.
Refactorings, no matter how big or small, should always leave your tests in a passing state. Therefore, your system correctly working exactly as before. Automated testing running frequently will give you confidence that your users will be unaware of any modifications to the internal structure of your code and help you quickly find and prune bugs that you may inadvertently introduce while refactoring.
Testing also helps organize a clear set of expectations of the code, which helps other developers whenever they identify some that requires refactoring.
Another tool to establish expectations is written documentation, complemented by a clear and standard naming convention across your platform. Indeed, if you lack a naming convention and can’t understand what a variable, method or class does by looking at the name, you have found the first opportunity for refactoring.
Where do I begin?
When you’ve set up the grounds for successful refactoring, a few tips will help you on your way:
Your tool belt
Refactoring is not just naively changing code to make it “better” (which is a vague goal). In fact, there’s actually a clear, defined set of transformations that you can apply.
Have you ever picked up a variable and modified the name to be more descriptive, then updated all the calls from the old name to the new one? This is called “Rename variable” and is probably the most common refactoring there is. If you’ve found a one-line function that was being used only once, and you eliminated this indirection by simply computing the variable inline, you’ve applied “Inline function”. If the function has no more uses, then you can apply “Remove dead code” to eliminate unnecessary definitions. These, and many other transformations, have a set of rules you can follow to guarantee a safe refactoring.
The trick is, you don’t need to master every refactoring there is from the start. Instead, think of these transformations as individual tools that don’t follow a specific order, but a specific need. You don’t need to learn how to use a hammer in order to paint a wall.
To understand which code needs refactoring and what transformation to apply, you’ll often find people referring to “code smells”. A “code smell” is not just a gut feeling that something is wrong. It’s actually a set of anti-patterns that usually lead to problems in software development, such as obscure, long methods or code that is repeated over and over again.
Code smells don’t tell you what to do, but they’re an indicator that something should be done. In identifying a code smell, you can also narrow down the set of transformations that you can apply to remedy the anti-pattern. For example, if you find a chunk of code repeated in a number of places (a problem known simply as Duplicate Code), you may want to try Extract Method, Extract Superclass, or Extract Class, patterns that fit the problem under different situations.
You’re not alone
Refactoring as part of the development process is a mature and well-understood technique. Your IDE is probably already able to perform some safe, automated refactorings in your codebase, depending on your platform, such as renaming a variable (Rename Variable).
Code review or peer programming is also a great way to identify opportunities and risks around refactoring. Bringing a new pair of eyes to the code is often a prime situation to detect code that smells and propose new patterns.
The most comprehensive catalog where you can find a reference to every refactoring and code smell is probably the book “Refactoring” by Martin Fowler (2019) which also has an online version. If you’re more of a course-oriented person, try their wonderful interactive course created by Refactoring.Guru.
When is it a good time to do code refactoring?
With all this in mind, it’s important to understand not all “smelly” code requires refactoring. Like mentioned before, refactoring has risks, so it should always be done purposefully.
Once you find a case of “code smell”, you may still want to take a few minutes or longer to ponder whether or not it’s worth the time and risk. Some well-founded reasons to refactor are:
Have you ever had to add a new feature to an application that worked 90% just like another one, but couldn’t reuse any of the previous code, so you just duplicated the module with menial changes? Or maybe a small bug took you a couple of weeks to figure out, then a one-line change to fix and an eight-line comment to explain? I know I have. Looking back to my initial days as a software engineer, I realize most of these experiences would have been a lot different if I had refactored first. I would take the time to understand the logic and capture that knowledge into an improved code structure before trying to add even more logic to muddy code.
In this spirit, Kent Beck, another prominent figure in the world of refactoring, dropped this piece of wisdom via his twitter account back in 2012:
for each desired change, make the change easy (warning: this may be hard), then make the easy change
— Kent Beck (@KentBeck) September 25, 2012
I would want to make one modification to that statement:
Because sometimes you just don’t need refactoring. You can avoid refactoring if:
Sometimes I run into any of these cases and still find it necessary to capture some understanding of the code. To do so, I trust means external to the code, such as documentation or a tutorial.
Ready to make the best of your codebase?
An organized, clean software system requires consistent maintenance and a flexible perspective on the code and its structure. The traditional workflow of minutely planning the entire system ahead of time is evolving. We’re heading to a new, modern, agile workflow of incremental steps and continuous improvement. Based on this, developers can constantly improve and adapt code design.
Refactoring is a first-class citizen of modern development. It’s distinct from, but complemented by, processes like debugging, feature development and performance tuning.
Now that you know that refactoring is not a vague idea of changing code in the name of perfectionism, but a structured set of tools and techniques to improve software design without modifying the observable behavior, you can start to lay in the groundwork for refactoring in favor of maintainability and scalability. To make it work, you need a comprehensive test suite, and clear expectations of the behavior.
You can refactor as part of your normal, daily development process. To do so, you should count on a version control system that you can use to jump back to a working state should things get out of hand. Not all code can or should be refactored. So, I encourage you to take the time to consider the purpose of refactoring and weigh the risks before you dive in.
And remember: Clean code is a journey, not a destination.
Feel free to share this post with your colleagues 🙂 We’d love to help more development teams with refactoring practices.