Michael C. Feathers defines legacy code as code without tests. I find it’s also likely, as a consequence, to be heavily coupled, with deep tendrils interconnecting multiple components. Indeed, many of the patterns in his Working Effectively With Legacy Code are about how to sever those tendrils safely in order to test the components individually.
But sometimes it’s about cutting out dead wood, that once-central code that’s outlived its usefulness and whose self-importance is infecting global configuration and the architecture.
The code that has special conditions in the API and would likely be more stable if it was split into smaller chunks.
Many of the techniques are the same as you’d use to get the system under test, add wrappers so you can redirect to null implementations, ready to remove all the calling code; replace global variables with more appropriate scoping.
So you get the code working, and passing the tests, so you’re comfortable that it’s removed, but then you have to deal with the orphans. The homeless configurations, the stateless variables, the untouched methods, that look like they should be free to go, and aren’t referenced anywhere. Except there’s some stringly typed calls, or rendering of inputs, that might, just might, use that code.
And they sit and rot.
Until you profile, and analyse, and kill off some more.
And you rewrite the input handlers to ensure nothing can be constructed to hit them, and kill off some more.
And then something breaks, and you’re too scared to touch any more, because of quantum effects.
And then you add new generic error handling so you can remove some more.
And then you rewrite it.