Skip to main content
1 of 7
user avatar
user avatar

I'll give a very informal and possibly unusual answer as one who has come from a similar background about a decade and a half ago in ways where the techniques described in Working Effectively With Legacy Code were inapplicable in large part because the team and management were unwilling to accept them.

I was that kind of outspoken young guy insisting on using profilers, writing tests, etc, preparing lengthy presentations which often had some colleagues rolling their eyes. In retrospect I was also rather dogmatic since I saw the existing codebase as the worst thing imaginable and in the foulest ways that Michael Feathers himself probably might not have anticipated. He talked about monster methods spanning a couple hundred lines of code. Let's talk about countless C functions that span over 20,000 lines of code with sporadic goto statements, over 400 variables all declared at the top of the function and uninitialized, and colleagues that get pissed off if you initialize them to try to make the runtime behavior more deterministic while swimming through uninitialized variable bugs because they're afraid of the computational cost of initializing a plain old data type (ignoring the fact that optimizing compilers will generally optimize away redundant initialization).

There's more to this than just software engineering. There's your own sanity at stake if you try to fight too hard against the tide. It can help to relax a little if you feel like your team isn't cooperating and become a cheerful pragmatist and take pleasure in more things in life than a codebase built using the most sound engineering principles, like naked ladies and beer.

Anyway, for a start, monoliths or God objects start to undermine coupling and cohesion in a similar way as having a bunch of global variables. When your program is tripping over state management and finding it cannot effectively maintain invariants, then the difficulty of correcting those issues is proportional to the scope of the state involved. Just as a global variable might be accessed by hundreds of functions, so too might a class member if the class has hundreds of methods which have access to this persistent state. If the state violates a conceptual invariant and causes bugs, then your list of suspects is proportional to the number of functions that can modify the state, class methods or not. Explaining things this way might convince your colleagues to take it easy on the 'God' objects and start hoisting functions out of a class that do not need access to the class's internals as a compromise.

As for embracing a test-driven mindset, in my case the codebase was too foul and the team was too uncooperative to apply that mentality. You need the entire team really on board including the management if you want to start creating effective unit and integration tests on a codebase that requires 7,000 global variables to be initialized just right by sporadic functions and a main entry point which, itself, is over 80,000 lines of code, just to test a small section of the codebase.

I tried, one time, just over some weekends to construct a unit test of the licensing system for our software in a standalone project that tried to just come up with the minimal code required to test it and a reasonable interface to test against (the former license interface was monolithic and included even sporadic GUI functions). I ended up having to pull in around 800,000 lines of code since just about everything was coupled with everything else just to verify a license code with an initialization process that required dozens of hours of trial and error to figure out what global states needed to be effectively initialized to do so. I can exchange horror stories all day long about nasty codebases from that former experience.

If you are in such a scenario, maybe the best option is to leave. That's what I eventually did. That said, I've found it useful for sanity purposes to seek to recreate key sections of the software almost from scratch, only using the old code for reference purposes. The goal is to come up with a completely independent library that you can test which recreates crucial functionality required for your software. It might not be the most productive way to go about it, working completely outside of your software on code that is not going to immediately benefit it, but at the end you get this independent library you can use and confidently demonstrate to your team, showing that it works conceptually through the tests and perhaps convincing enough people to replace large sections of code with your library which is now shown to be effective and reliable. It's a good confidence-boosting exercise if you are swimming in a foul sea of hopeless complexity.

user204677