When we use the words “refactoring” and “maintenance,” there is often a common understanding of what concepts we are referring to. While these words seem to describe the concepts very well, we cannot be sure that their meanings are perceived in the same way. By taking a deeper look into this topic, I will be sharing my thoughts on the subject. Let’s try to define “code maintenance” and “code refactoring” before going into greater detail about how they are related.
Refactoring is the process of modifying code without changing the external behavior of the software. Most of the time, the initiative to refactor comes from architects and developers. The development team generally knows what improvements the system requires and is familiar with the technical debt they are facing. Examples of common technical debt encountered include:
- The unit’s original design was too customized and not generic enough
- Chosen technology had some limitations that became obvious later. At the time, redesign was not feasible, so a workaround was implemented
- Poor decision of an inexperienced developer was overlooked, which created messy code that was easier to redo from scratch
- New technology that is much more suitable is available
- Changing requirements leading to messy code
- Some of the code over time was touched by several developers with limited knowledge transfer, creating layers of different coding approaches
- Placeholder code
- Code was not unit tested due to time constraints
- Last minute dirty fixes
- Fixes of problems found by static code analysis tools
The team should keep track of technical debt. Ideally, time should be allocated for technical debt reduction post release. Refactoring tends to create significant changes that have a high potential to reduce code quality. The beginning of the next release cycle is the best time to make those changes.
Simply put, code maintenance is post-deployment code modifications that mitigate or prevent problems. Code maintenance always requires some code changes. Those changes can be split into two categories–Functional and Non-Functional Changes.
Functional changes may alter functionality of the software and are usually initiated from the outside, including:
- Bug fixes
- Security improvements
- Performance improvements
- Adaptation to environmental changes
These changes usually require some deployment work (e.g. patches, hot fixes, and backporting).
Non-functional changes don’t change the functionality of the software, but improve it internally, such as:
- Design improvements
- Code quality improvements
- Build process improvements
- Maintainability improvements
- Test improvements
Code refactoring belongs to the non-functional category. In fact, we can say that non-functional changes and refactoring are the same.
Why do we refactor the code? There can be many reasons why people conduct refactoring. We will now discuss some of the most common reasons.
If a project was built using the latest technologies, such as languages, tools, frameworks, libraries and methodologies, there will always be a risk of costly refactoring early on down the line. New technologies tend to evolve at the beginning, which can lead to revolutionary changes in their design, or even more frightening, in their interface. If your project has a lot of code written using the first version of some new framework, and then the provider of the framework decides to change the way it interfaces with the consumer (your project, in that case), you will have no choice but to reimplement a significant portion of your code. When a new technology comes out, it’s better to suppress the temptation to jump right on it. Let it mature for a bit.
Cleanup after Another Team or Project
A big challenge is when you have to pick up an existing project from another team. Most of the time, this transition comes with a lack of documentation, little-to-no knowledge transfer, or a previous team that is too busy, inaccessible or uncooperative. Sometimes, the new team can have a different set of expertise. Consequently, all of these possible scenarios can easily lead to a decision to reimplement the whole system or a big part of it. There is also the added risk that the new team, due to a lack of understanding of the existing system, underestimates the complexity of the system.
Ugly code can also be caused by infrequent collaboration between groups, when different people implement the same function due to a lack of effective communication.
From the client’s perspective, refactoring is some strange activity that they are asked to pay for, but with no new features being added. Some clients may be reluctant to approve such activities. Sometimes, refactoring tasks can require massive changes. For example, if we want to replace a library that is used throughout the system, we first need to analyze where it’s used, its different patterns of usage and how replacement libraries can be integrated in all the cases.
Code Quality Degradation
The greater the number of changes, the greater the number of bugs added. Global changes, like migration to another platform or replacement of third-party modules, will introduce instability in code quality. No project has a perfect regression test set, and even if it does, many tests will be specific in the way the system is using technologies that are being replaced. New technologies will inevitably introduce new scenarios of usage, and make other scenarios obsolete. In any case, the regression test set will become obsolete, and successful execution of it doesn’t mean that system has the same quality as before. You have to accept the fact that quality will go down and will require some time to improve by trial and error. This factor is one of the reasons why it is hard to sell such improvements to the clients.
Minefield of Opinionated Frameworks
Opinionated frameworks help with rapid development by hiding a lot of the piping and making code cleaner. While useful, they rely heavily on reflection to do autoconfiguration. When it comes to the upgrade, they can cause unexpected problems because frameworks themselves can change internally and start to configure the application differently. As a result, problems can be hidden and hard to pinpoint. Since the entire system relies on the framework, the update can cause large scale refactoring.
Development Schedule Impact
When a team is on a tight schedule, corners can be cut, optimizations can be saved for later, unfinished code can be left in the code base, for instance. This incurs technical debt that should be addressed in the future.
Increased Operational Complexity
As the system is in production, real world problems, such as bugs and maintenance and performance issues, will begin to emerge. To solve these problems, operational solutions will need to be implemented. Examples include:
- Integration with another system that software was never designed for, but can be done using some “hacky” way
- Temporary file cleanup—logs rotation, archiving, and deleting
- Platform reconfiguration to overcome limitations of the software. System crash recovery
As the list of complaints grow, it becomes obvious that many of these problems can be solved internally. Internal solutions often can be more elegant, less complex and more future proof.
Refactoring modernizes and improves software, but any serious changes come with associated risks, including:
- Introduction of new bugs
- Possible new performance issues
- Introduction of security risks
- Changes in administration and maintenance practices
- Underestimation of efforts
The need for refactoring is inevitable. We need to be ready for it. There are number of ways we can organize the code to make refactoring easier. These include:
- Leaving parts of the code, in some cases, unfinished due to time constraints or implementation uncertainties. In this situation, we should try to design code to isolate unfinished parts and clearly provide comment sections that need to be implemented later
- Using development practices to minimize the need for refactoring
- Performing periodic code reviews. During the code review, reviewers can recommend better ways to implement certain parts of the code that the developer implemented inefficiently. This effort will minimize the need for refactoring
- Making use of code complete features of IDEs
- Using static code analysis