Martin Fowler says that we should do refactoring before adding new features (given that the original program is not well-structured).
So we all want to refactor this dirty codebase, that's for sure. We also know that without unit-testing code it's very easy to introduce subtle bugs.
But it's a large codebase. Adding a complete suite of tests to it seems impracticable.
What would you do in this case?
See Rands in Repose's Trickle Theory for a similar problem he encountered with huge numbers of bugs. His recommendation:
Don't think of it as an either/or proposition. There is value added by adding unit tests--you don't have to add an entire suite to get the benefits of adding some tests.
I find myself in this situation quite frequently, as that's pretty much what I've been stuck with for the last two years.
The right approach really depends on social and organizational aspects more than on the technical side of things. Your job is to generate value for your organization. If refactoring will generate more value than it costs, then you should be able to sell it. In my case, the key factors include:
Expected ownership of the project in question. If you expect to be a significant stakeholder in this particular piece of software for the forseeable future, that's an argument in favor of making more extensive modifications to a bad code base b/c it'll pay off more as you maintain it going forward. If you're adding a drive-by feature, adopt a more hands-off approach.
Complexity of the changes being made. If you're making deeply complex changes to the codebase (a typical case in a "dirty" codebase, b/c such source is generally tightly coupled and incohesive), some refactoring is more likely to be called for. Such changes aren't the result of being a code ninja, either, as they're necessary for you to simply reason about the changes that you're making. This is also related to the "badness" of the codebase you're modifying. It's practically impossible to create even the simplest unit tests for a tightly coupled, incohesive mess. (I speak from experience. One of the projects I almost got stuck maintaining was about 20k lines long, excluding generated code, in twelve files. The entire app was a single class called "Form1". This is an example of a dev abusing the partial class feature.)
Organizational oversight. The strength and strictness of your organization's oversight comes into play here. If your group actually performs some core best practices, such as code reviews, and doesn't just pay lip service to them, I'd be more inclined to not make extensive refactorings. The value tradeoff is probably weighed more in favor of touching as little as possible because you've got another pair of fresh eyes checking to make sure that none of the few changes you did make have undesirable side effects. Similarly, stricter oversight is more likely to frown on the "guerilla" code tactics of making changes that aren't strictly called for in the change request.
Your boss. If your boss is on your side, you're far more likely to be able to make long-term value-improvement on your codebase, especially if you can justify the increased cost now in terms of budgetary hours later down the road. Remember that your manager has a better perspective on this software's role in the big picture than you do. If it's a piece of software that's only used by ten or twenty people, it just doesn't call for the sort of long-term maintenance improvements that a piece of software used by ten or twenty thousand people calls for.
The core question you need to answer when considering any sort of time investment like this is, "Where does the value lie?" Then you need to chase that value.
My suggestion would be that you touch as little as possible and add what you need. I have found that it is best to leave well enough alone, especially if you are on a tight deadline.
If you had had unit tests, that would be a different story, but when you change code it can be like touching a spider web. Changing one thing can affect everything else.
Fowler also suggests that you never refactor without the safety of tests. But, how do you get those tests in place? And, how far do you go?
The previously recommended book (Working Effectively with Legacy Code by Michael Feathers) is the definitive work on the subject.
Need a quicker read? Take a look at Michael's earlier article (PDF) of the same name.
If adding a suite of tests is impractical, then you have a serious issue with the codebase.
Adding new code and hoping it Just Works is bad.
Write the tests, refactor the base, and add new code. If you can't test it, you'll never know if it's correct.