Kilian's answer covers the most important aspects, but I want to expand on points 1 and 3.
If a developer wants to change (refactor, extend, debug) code, she has to understand it. She has to make sure her changes affect exactly the behavior she wants (nothing in the case of refactoring), and nothing else.
If there are tests, then she has to understand the tests as well, sure. At the same time, the tests should help her understand the main code, and tests are far easier to understand than functional code anyway (unless they're bad tests). And the tests help show what changed in the behavior of the old code. Even if the original code is wrong, and the test tests for that wrong behavior, that is still an advantage.
However, this requires that the tests are documented as testing preexisting behavior, not a specification.
Some thoughts on point 3, too: in addition to the fact that the "big swoop" rarely ever actually happens, there's also another thing: it isn't actually easier. To be easier, several conditions would have to apply:
- The antipattern to be refactored needs to be easily found. Are all your singletons named
XYZSingleton? Is their instance getter always called
getInstance()? And how do you find your overly deep hierarchies? How do you search for your god objects? These require code metrics analysis and then manually inspecting the metrics. Or you just stumble over them as you work, as you did.
- The refactoring needs to be mechanical. In most cases, the hard part of refactoring is understanding the existing code well enough to know how to change it. Singletons again: if the singleton is gone, how do you get the required information to its users? It often means understanding the local callgraph so that you know where to get the information from. Now what is easier: searching out the ten singletons in your app, understanding the uses of each (which leads to needing to understand 60% of the codebase), and ripping them out? Or taking the code that you already understand (because you're working on it right now) and ripping the singletons being used there out? If the refactoring isn't so mechanical that it requires little to no knowledge of the surrounding code, there's not use in bunching it up.
- The refactoring needs to be automated. This is somewhat opinion-based, but here goes. A bit of refactoring is fun and satisfying. A lot of refactoring is tedious and boring. Leaving the piece of code you just worked on in a better state gives you a nice, warm feeling, before you move on to more interesting stuff. Trying to refactor an entire code base will leave you frustrated and angry at the idiot programmers who wrote it. If you want to do a big swoop refactoring, then it needs to be largely automated so as to minimize the frustration. This is, in a way, a meld of the first two points: you can only automate the refactoring if you can automate finding the bad code (i.e. easily found), and automate changing it (i.e. mechanical).
- Gradual improvement makes for a better business case. The big swoop refactoring is incredibly disruptive. If you refactor a piece of code, you invariably get into merge conflicts with other people working on it, because you just split the method they were changing into five parts. When you refactor a reasonably-sized piece of code, you get conflicts with a few people (1-2 when splitting up the 600-line megafunction, 2-4 when breaking up the god object, 5 when ripping out the singleton from a module), but you would have had those conflicts anyway because of your main edits. When you do a codebase-wide refactoring, you conflict with everyone. Not to mention that it ties up a few developers for days. Gradual improvement causes each code modification to take a little longer. This makes it more predictable, and there isn't such a visible time period when nothing happens except cleanup.