Ayende mentions, "Working Effectively With Legacy Code." This is also one of my favorite books. I read it just before starting my current job, which was kind of good timing.
I see this book as kind of a self-realization kind of thing - no matter how good of a developer you are, at some point while reading this book, you will realize, deep down inside, it's not just other people writing legacy code - you're writing it too. What are you going to do about it? Legacy code is all of our problem. It doesn't help to blame people, because we're all to blame. It's only legacy code if you can't change it and make it better. Blame isn't a part of making it better.
It's very healthy for people to have these kinds of realizations every so often. We all need it.
I, like everyone else I'm sure, am currently in the midst of dealing with some legacy code (LDI). At the same time, I am thinking about some changes to a project I'm in charge of, but had to put down for a while recently (Select Holdings).
One of the big themes in Feather's book is breaking dependencies. Repository and Service Locator are great patterns for this. I particularly like the implementation Ayende has built in Rhino.Commons. There is a kind of Service Locator class called IoC, which makes it drop dead simple to pull out "new"s from legacy code and make them services. This plus his Repository<> class, which is built on the IoC class, help get services and entities injected into dependants with absolute minimum change to the dependant code - i.e., no new constructors or properties to add, and the corresponding ripple effect that has on the code you are trying to work on.
For example, one of the classes in the legacy code I am inheriting is kind of a gateway for a third party service we pay for. I really want the interface code - it does all these nice things like RPC over FTP and translating weird file formats - but it has about a dozen yucky dependencies on some code I have no time to maintain. I am talking about lots of speculative generality - six classes necessary to represent "price" in the domain model. Plus lots of static methods.
My refactoring steps become:
0. Write some lame tests that actually call the third party service and inflate our bill, since there are no existing tests to speak of.
1. Replacing new's with with IoC.Resolve<>() and Repository<>.Get(). This works especially well in cases where there are a ton of static methods.
2. Test. Last test we have to pay for.
3. Extract Interface on the dependency. Replace IoC.Resovle<SomeService> with IoC.Resolve<ISomeService> (the interface).
4. Use Rhino Mocks to mock the interface.
5. Test.
6. Build new simple implementation of replaced service, this time with tests and better design. No more changes needed to refactored gateway code that I want to keep. If I do need to make more changes, at least I have tests now.
I keep hearing the "Six-Million Dollar Man" theme song playing in the background. That's what this feels like. Instead of the despair of, "Crap! I have to maintain more legacy code that I didn't know about!" around every corner, there is the positive thought, "We can rebuild him. We have the technology. We can make him better than he was. Better...stronger...faster."