So what is technical debt?
There are loads of definitions – I think of it as code that is difficult or dangerous to change or extend. Technical debt is often associated with legacy code, which Michael Feathers defines as code without automated tests, though i’ve seen legacy code that was straightforward to change, and code with automated tests that wasn’t.
There’s a surprising amount of code with tests thats difficult to change or extend. Often dependencies have not been isolated well, so tests have extensive scaffolding. Another reason is overly complex or poor designs – if I find i’m changing every class in a subsystem for a simple addition, there is often a design problem.
The issue for me is not that technical debt exists (we’ve all written some – or at least I have), but how to address it. I think its as much a people problem as a technical one and there are many reasons for it
Managers discourage changes to existing code
- Some managers don’t understand why existing code should change to accommodate new features. They ask why couldn’t you get it right first time? Ironically the more successful a product, the more likely it is to change and grow, often in unexpected ways.
- Managers who have been developers in the past, know the dangers of making changes to code that has no tests, and shy away from making changes for that reason.
- Others have been burnt by teams that got bogged down performing epic refactoring’s or rewrites
- Finally, there is the ever present pressure of the roadmap, which managers are typically far more exposed to than any individual developer. Under that pressure, its tempting for even the best manager to encourage shortcuts.
We want to work on new code
- Old code still needs maintaining while the rewrite is happening
- In agile teams where backlogs can change rapidly its easy for large rewrites to stall or be abandoned as priorities change.
- If you are dealing with a codebase that has a lot of duplication, it may be better to reduce the duplication first, rather than introduce yet another mechanism for doing something
- If the code is reasonably modern and has decent test coverage, refactoring is often by far the safer route – though some developers still argue for a rewrite
We don’t recognise that there is a problem
We’re scared to touch it
We don’t have the skills
What to do about it?
Skills and Training
- If the method in question does not reference any member variables we can make it static and thereby write tests for it without having to instantiate the whole class. Ugly but effective.
- We can subclass and in the derived class override selected methods to effectively null out dependencies
- We can add setter methods to override dependencies.
- We can make private methods public to get access to them (!)
- We can link to mock libraries
- When adding behaviour we could create a small object with the new behaviour using TDD and then just call it from the legacy class. In the short term this can be pretty ugly – maybe the new class has only a single method; but over time we could move behaviour as appropriate from the legacy class to the new one.
These techniques are highly incremental and can make the code feel worse in the short term. Maybe thats why they are used as much as they could be.
I learn a lot by doing dry runs. Check out the code and try out a few refactorings. Then throw that code away and try it for real. Don’t be surprised if your real refactoring works out differently – the point of the dry run is to gain confidence and context.