Sergey Nivens - Fotolia
Refactoring is a vital part of software code maintenance. This restructuring process improves the code's readability and extensibility -- it might even address looming flaws before users experience their effects.
However, there are significant risks for organizations when they opt to refactor code, so they must approach the task with caution. Improperly refactored code can inadvertently change the software's functionality, introduce flaws, demand additional troubleshooting and alter performance. Each code refactoring mistake can be a recipe for UX disaster.
When to refactor code -- or not
When you refactor code, follow these 10 tips to avoid costly mistakes or rework.
- Fix software defects separately.
- Avoid new features and functionality.
- Refactor only when it's practical.
- Understand the code.
- Bring uniformity to coding practices.
- Refactor -- and patch and update -- regularly.
- Set clear objectives.
- Focus on code deduplication.
- Test early and often.
- Use tools that help improve code.
These code refactoring best practices detail when to refactor vs. bug fix vs. build new, how to allocate time for the task, ways to define scope, and how to bring automation into the process.
Keep in mind that these code refactoring tips are not necessarily sequential -- some will likely occur simultaneously. And not every team needs every tip here -- assess where you need improvement and focus there.
1. Fix software defects separately
Code refactoring is not troubleshooting or debugging. Thus, the process cannot address software flaws by itself.
Code refactoring corrects issues with application design or implementation. The process is a way to remediate code smells: undesirable code attributes, such as excessively long routines or separate routines that are nearly identical.
Software defects often carry through the refactoring process. If a bug exists before code refactoring, it will likely remain afterward. Thus, the right time to perform refactoring is once the software's bug rate -- tracked via the number of help tickets, support requests or another measure -- is acceptably low. Refactoring should happen on a mature codebase.
Developers who keep bug fixes and refactoring separate know where new issues come from. If developers refactor and debug code at the same time, it's virtually impossible to determine the cause and effect relationship in the new version of the software.
Some issues, however, require code intervention. For example, if a module in the codebase throws unpredictable exception errors when it interrogates certain hardware devices -- despite repeated efforts to remedy the problem -- that's when the team should refactor. In this example, developers already know the source of the problem, and refactoring might fundamentally rework the way in which the module queries those devices.
2. Avoid new features and functionality
Refactoring is a form of housekeeping developers perform to make code clearer, cleaner and more efficient. The process is not an opportunity to change functionality or add features. Save new features and functions for after refactoring is complete.
A test-driven development (TDD) paradigm called red/green/refactor reflects how to refactor code:
- The dev team sets a goal, which software does not currently meet (red).
- Developers add to or change the software to meet the goal or pass the test (green).
- They clean up the code before the cycle repeats (refactor).
Segregate refactoring and feature development, just as you did with defect fixes and refactoring, to reduce unnecessary troubleshooting and limit risk. Refactored code should not change anything; the software should exhibit the same behaviors and pass the same tests. If behavior changes, code refactoring work is often the culprit. The dev team must roll back to the previous version.
3. Refactor only when it's practical
Code refactoring requires software development resources. Developers can't create new features and perform maintenance at the same time.
Cleaning up the code is time-consuming and costly. Evaluate the software lifecycle and roadmap to determine whether it's more efficient to refactor or create a new build from scratch.
Refactor when a modest developer investment yields more efficient, readable and maintainable code. Developers might refactor code when they want to reduce the number of processor or I/O cycles to complete, or improve both compatibility and stability. Rather than make repeated calls to obtain the same state, for example, refactored code could make a single call, with a variable to retain the state one time and then simply reuse that variable as opposed to making repeated calls.
It's easy to lose control of the project's scope. These two code refactoring examples demonstrate value with limited investment:
- Standardize variable names and nested formats to make code more readable.
- Modify one routine to do the work of several similar routines, shrinking the software's memory footprint.
Redesign and rebuild with new code, based on current standards and practices, when the scope doesn't fit refactoring. Sometimes code is too old or convoluted for developers to make sense of it. Also, a change to the hardware platform could go beyond refactoring's scope to make the software work. And code refactoring is too unpredictable to fit into time-sensitive projects.
4. Understand the code
As with any software development initiative, developers define the goals of a refactoring project upfront. Figure out what the code does and how it works before you start.
Review the code to understand its processes, methods, objects, variables and other elements. Ensure junior developers and anyone unfamiliar with the product is comfortable with the codebase.
Meet as a team to discuss and annotate the code carefully. Highlight areas of interest -- sensitive routines, variables, objects or methods -- and then fill in any missing comments.
5. Bring uniformity to coding practices
Coding is usually a team sport, with each developer responsible for separate parts of the codebase. However, developers bring a unique approach and preferences to their tasks. As the code changes from individuals' input, the overall codebase becomes difficult for developers to read and understand. This confusion adds tremendous time and cost to troubleshooting and debugging.
Create the most readable code possible when you refactor. Apply consistent coding techniques across the entire development team. For example, add meaningful comments, indents, line spacing and other readability formatting to make software code easier to comprehend. Streamline the coding approach for the future and iron out irregularities to make code easier to maintain.
6. Refactor -- and patch and update -- regularly
Code refactoring helps address technical debt: any shortcut or workaround in code that creates a short-term gain but long-term issues. As with financial debt, technical debt requires investment to reduce it, and some methods are more effective than others.
Code refactoring generates the most return on investment when it can address a significant issue but without much time and effort. Regular update or patch cycles help development teams realize more value in their refactoring efforts. Modern software development cycles include regular patches and updates. With this cadence, developers naturally learn the codebase as well as when and where to refactor code.
Ideally, developers shouldn't refactor any code concurrent with patch or update cycles, but they can make refactoring requests iteratively. Project managers then prioritize the requests and create a refactoring to-do list. The list informs how they allocate development resources to the most meaningful and effective refactoring tasks.
7. Set clear objectives
Refactoring is rarely a one-time, all-or-nothing effort. In practice, how a dev team refactors code can mean many different things, with varied levels of complexity. It's easy for a developer to stray from unclear objectives. So, set a clear scope and goals early in the code refactoring initiative to avoid delays and extra work.
Delineate what, how and when to refactor code. Consider what the refactoring task will -- and won't -- include. Set small and specific goals that don't depend on other tasks. Establish timelines and delivery objectives that fit within existing workflows, such as CI/CD processes. For example, a developer refactors a vital subroutine to function more simply and efficiently, and stops there. And later on, developers can break the large method into several small routines. In another example, initiate a refactoring project to eliminate hardware dependencies, such as directions to check for specific processors, to decouple the app from its infrastructure.
When setting the code refactoring scope, choose a development approach, such as TDD or behavior-driven development (BDD). With a set development approach, the team can refactor code with an eye toward testing and ensure that the application will continue to operate in a normal manner. The refactored code should pass TDD, BDD or other tests unique to the approach. If it fails any tests, an error or oversight might have occurred. Developers should then debug accordingly.
8. Focus on code deduplication
Once you know when to refactor code and what a typical project entails, train developers on the best candidates for refactoring. One of the most obvious is duplicate functions that use two or more methods or routines to achieve similar results.
Duplicate or similar functions pose three distinct problems for code:
- Duplications expand the software's footprint, wasting system resources.
- If you make changes to one routine, you might also need to alter others. Otherwise, logical or functional errors could threaten the software's stability or security.
- Duplication adds complexity, which is a burden during test, debugging and troubleshooting.
Combine similar or duplicate functions into a single method to create better code.
9. Test early and often
While code refactoring doesn't change software functionality, it still requires test coverage. Testing checks that refactored code functions as intended and doesn't introduce any flaws.
Every time a developer completes a refactoring task, test the code. Refactoring tasks with multiple passes or phases should undergo multiple rounds of testing. And always maintain a rollback plan.
The established tests for the codebase should not change as a result of refactoring. If tests fail, that likely means you introduced a bug. However, a test can also fail if the criteria no longer apply to the refactored code. For example, a code refactor changes object classes, such as some low-level objects subsumed into a higher class. In that situation, rewrite the tests. Such issues usually don't affect BDD tests, because the test evaluates the software's outward behaviors.
10. Use tools that help improve code
Software development tools can help automate code refactoring tasks. These tools accelerate refactoring and catch common code issues that might otherwise go unnoticed.
Some examples of tools include code editors and IDEs for a variety of programming languages:
- Eclipse and IntelliJ IDEA for Java;
- Visual Studio for .NET and C++;
- Wing IDE for Python; and
- Scientist for Ruby.
Editors and IDEs also help developers adhere to code maintainability standards, such as consistent code formats and variable name conventions. These details, while often overlooked, reduce refactoring in the future by enforcing rules upfront.