So, you got to know what are the consequences of bad code (here & here). You also got to understand how you can objectively decide if a particular piece of code is horrible or not (here). Then comes the question:
Ok, I have inherited this devilish system. What should I do to improve it aside from nagging to my manager?
Luckily, again, the software industry is experienced in dealing with such Brownfield projects. But before discussing how we can improve a brownfield project, let us explain why I believe it is a must skill to able to fix and refactor legacy projects. Let’s discuss what refactoring is, and then we make a case for it.
What is Refactoring?
Refactoring is the process of restructuring existing computer code—changing the factoring—without changing its external behavior
Simply put, make my code easier to understand and change without changing its business behavior. Uncle Bob made it on the spot!
Since I love philosophy and theoretical discussions, I always find it interesting -and also useful- to start with the why question. Let us discuss why we need refactoring in the first place. From WHY questions, we build wisdom!
Being a good citizen
I always believe in “Make Your Code Cleaner than Your Found It.” Refactoring and cleaning code is a healthy sign of a professional developer; it tells that you care about the product, your employer, and, most importantly, your work quality and professional standards. Not only that, it will put you in a position to be a leader by doing, which is a way stronger position than leading by fluffing or talking all day about quality and so. Remember: Actions are louder than words.
You will work with legacy systems MOST of your careers
Software systems are expensive; we all know. And when they are developed, they are meant to stay for years to come until they recover their ROI. If it is an HR system, the company wants to make sure that they save money on automating manual processes, having fewer errors, and obtaining a better experience. If it is integration, the company wants to save time on less data quality errors, centralized management, and quicker processes. At the end of the day, business is just a simple math game, and every software system must comply with it; otherwise, it does not worth it!
So, if you read studies about the total cost of ownership (TCO), you will find that research suggests that 80% of the software life cycle costs are actually maintenance costs, while the 20% is the development or acquisition cost. Dump math will tell you that out of each five projects you work on, only one will be a Greenfield project. If you managed to find such a project, believe me, you are fortunate!
Therefore, the fact that you will most probably work on a legacy project simply means that you need to learn how to read a lousy code someone else wrote, and most importantly, how to improve it!
Reducing technical debt
Technical debt is one of the most dangerous things that the business is usually unaware of. The more complex and rigid the code is, the more expensive it is to change. We have discussed that extensively in (here & here). So, if you refactor and optimize your code so that it becomes easier to change and extend, you are doing a big favor for the business! It means better growth opportunities, better customer reaction, and better time to market.
What are the common Refactoring techniques?
One common refactoring technique is inspired from the test-driven development approach (TDD) and follows three simple steps. It can be thought of as a general refactoring guideline.
- Green: Write just enough unit tests to make a small piece of the current code passes. This will give you the confidence in your code so that if something breaks, you can detect it.
- Refactor: Since you have backed-up yourself, refactor your code with confidence. If you break your code, your unit tests will hopefully fail (given that you have good enough code coverage).
- Red: The changes you introduced during the refactoring phase will likely break your currently working code, thanks to unit tests. You can repeat the cycle from here to fix the code and go again to the green state.
This process continues iteratively through the code base until we reach an acceptable level of code quality.
A simple and conventional way to do abstraction is by composing methods, that is, by identifying repetitive pieces of logic and extract them to separate methods. Common pieces of logic I have seen that need to be refactored are logging logic, file reading logic, and printing logic.
A common way to do refactoring by introducing a refactoring layer. Either in cross-systems integration scenarios or even within the same code base. A refactoring layer is typically introduced to protect developers against ugly, yet necessary code bases. A common scenario I have seen across systems is when integrating with banking systems. Most banks have introduced an API wrapper against complex mainframe interfaces that almost no one understands how it works. Other ways to do abstraction within the same code base is through class inheritance and extraction.
Moving Features Between Objects
An essential principle in object-oriented design is the single responsibility principle: That is, a specific class, method, or piece of logic should be responsible for the only thing. Therefore, having classes that do too many different things (e.g., calculating employee salary and printing it) is considered against ODD. There are a strong case and reason why this is unacceptable: Simply because it introduces unnecessary dependencies across the code and makes it difficult to change.
Is this image very confusing to understand right? Hhhh, yes, this is on purpose! Do not spend your time digesting it. We won’t go through it. The complexity you are facing to understand this diagram is too excessive conditional complexity. Many techniques help in reducing conditional complexity, such as replacing nested conditions with guard clauses and replacing conditions with polymorphism.
Convincing business to refactor?
Justifying refactoring to business is one of the most challenging tasks I have been into. The thing is that the business perception is that they are paying for something that does not deliver a “tangible” functional requirement from their point of view of course. The trick to this situation is to convince businesses with the language they understand, NUMBERS. Try to make a comparison on how long it takes to modify a specific feature with well-written code and another feature with ugly code and show them the percentage of productivity improvement. Yes, a tough and detailed task, but this is what it takes to convince non-technical people.
Moreover, I would like to ask to use refactoring responsibility not to overengineer your solution (have you heard of analysis-paralysis?) and focus only on refactoring critical, and most moving parts in your codebase. One excellent candidate for that would be an invoicing engine logic that requires to be adapted a lot to the market needs. Avoid refactoring non-changing parts such as CRUD logic areas unless you have a clear business justification. At the end of the day, remember, we are at dollars business!
Finally, I would like to conclude this post by sharing some resources that I believe help a lot in improving your refactoring skills.