There is some code. It doesn’t matter whether it’s mine or not, legacy or not, finished or not.
It can probably be refactored, and that may bring benefits.
- Reusability – by grouping code into appropriate classes and interfaces
- Easier maintenance – possibly improving the clarity and readability
- Improved Time/Memory performance – reduced duplication, for one thing
- Stability – reductions in cyclomatic complexity reduces possibility for bugs (?)
- Testability – encapsulation and better interfaces will enable isolating and mocking more components than before
The costs are usually along the lines of:
- Time – spent reading through all branches of the ugly code/tests
- More time – spent devising and implementing cleaner structures
- Even more time – spent verifying the refactor doesn’t introduce breakages
- Perhaps, even more time – spent retrospectively adding tests to ugly code if it wasn’t entirely well-tested, fixing bugs before/after refactoring and in the worst case – undoing all or part of the refactor, for example, if it introduces catastrophic bugs
Many will tell you to refactor incrementally; as you code. Many only do it as a special activity after the codebase is already over-inflated.
For me, both approaches have validity. Refactoring-as-you-code by adding sensible variables, converting complex data-structures into classes, adding interfaces rather than using raw classes and moving duplicate code into functions should be compulsory – these trivial steps don’t take time or even much thought to add value.
Refactoring-at-the-end is based on a tricky principle: I don’t know what all the code will do until it’s finished. The story might be: hindsight and exposure to the code has inspired a vision of the “cleaner” structure, to be forged by a hero. This is usually a terribly idea if it’s driven only by a yearning for elegant code.
In the past, I have plotted a refactored structure and started to implement it solo as the workload and pressure decreases. After 2-days in the zone, I emerged with a creaky overhaul. It creaked because my intention and focus was to show how elegantly the code could be restructured and reduced. I invented a task to serve a personal agenda – have you ever done that? It probably worked out okay, but I’ll bet there is some guilt associated with how long it took and how much it didn’t really matter?
My conclusion: you can refactor as you code and refactor retrospectively (after working code is produced) but do not refactor if the only reason is because you have a genius idea to show off. You will waste time, not add much value, introduce a few bugs, and annoy your team-mates. By all means, if someone has not been refactoring along the way and “technical debt” has accrued, or there is a measurable value gain (test coverage, reduced complexity, performance) then restrospective refactoring phase probably won’t cause burnout, catastrophic bugs or missed deadlines.
My preference is to consider the first iterations of the prototype as a learning exercise. Working code is being produced, tested and refactored along the way – this is software which could be used. If I’ve done everything right, the code should already have an optimal architecture because I’ve incrementally added functionality immediately into the correct place – no retrospective refactoring needed.
However, if I’ve dared to make mistakes, then I identify what happened and why, and fix it. This has happened many times: I start by making complete UML diagrams, identify the commonalities in calls/references and restructure accordingly (probably in MS OneNote).
(Note: think about whether getting a shiny, new UML tool is more useful than just manually making the UML in your case)
The point? Our code automatically gives us the answer when we ask “what should this software look like?” If you work in increments (e.g. Test-driven development) and are disciplined, you won’t have to compensate by altering the answer after-the-fact.
I am always learning – any comments, feedback or discussion is welcomed!