It's a known fact of life that code bases, if not actively nurtured, deteriorate with time. Maintaining, fixing bugs, extending, creating new features; all exert forces that push a design beyond its original intended boundaries, breaking the envelope of its original frame. Not long after its first release (sometimes, even before it), there's not even a trace of design, there's no conceptual integrity; duplication and needless complexity abound. Understanding and changing such systems is very difficult, technical debt never seems to decline. No position is more dreaded than that of the maintenance programmer. It's certainly not fun to work in these big balls of mud. Debugging sessions can take days, every time a change is required, there's fear that some unrelated functionality might suddenly break.
In the book Working Effectively with Legacy Code, author Michael Feathers gives wonderful advice and practical, to-the-point techniques to deal with these quagmires of rotting code. The book revolves around gradual improvements of the code, by covering it with unit tests, breaking its internal dependencies and slowly bringing it under control. Most of the examples use widespread object-oriented, statically-typed languages, like Java and C++, but its plethora of refactorings and heuristics are broadly applicable to a wide range of programming languages, even with different paradigms. The book defines legacy code as "code without tests", which brings about the shocking realization that many people are writing it right now.
Dependency-breaking techniques are at its heart. Their goal is to decouple areas of the code, so that they can be covered with tests. The tests are the instruments that allow changes to be made, and new capabilities to be added, with reassuring feedback that the code still does what it's supposed to do. It's a slow but rewarding process; where each step gives more understanding, and more courage to change the code. This is not an easy task, but the book has a way of making it an interesting one.
I particularly liked the emphasis on unit tests, and the fact that there's no debugging techniques section. Although a debugging session is a necessary activity at times, its results are not automatically captured; most of its goals should instead be described as unit tests. I find myself using less and less debugging sessions the more I write tests.
This is a timeless software book, that should have a place in the bookshelf of every software professional. It's specially important in large organizations, with big and unwieldy projects, but it's also applicable in small and medium development shops, if only to prevent the code from becoming legacy.