Michael Feathers' Working Effectively With Legacy Code starts out with the most useful definition of "legacy code" I've ever seen.
To me, legacy code is simply code without tests. I've gotten some grief for this definition. What do tests have to do with whether code is bad? To me, the answer is straightforward, and it is a point that I elaborate throughout the book: Code without tests is bad code. It doesn't matter how well-written it is; it doesn't matter how pretty or object-oriented or well-encapsulate it is. With tests, we can change the behavior of our code quickly and verifiably. Without them, we really don't know if our code is getting better or worse.
As someone who finds himself working without a lot of code that doesn't have tests, this resonates strongly with me.
So, how do we "work effectively" with code like this? By getting it under test. In particularly, by crafting unit tests that (1) run fast1 and (2) help us localize problems. Michael identifies two things we really need in order to write these tests. First, we need separation between modules. Most legacy code bases are hard to write units test for because of snarled dependencies. I've struggled through trying to instantiate a class in a test framework only to end up with half of the application in my test, so I understand the complexities involved. I've also dealt with code that we can't test in a framework because of undesired side effects from running it. Second, we need to be able to sense certain values that our code computes so that we know it did the right thing. Both of these are accomplished by what he calls a "seam":
A seam is a place where you can alter the behavior in your program without editing in that place.2
To exploit this notion of separating and sensing with seams, the book presents a set of dependency breaking techniques3 that can be done fairly safely to bring legacy code under test. The majority of the book is structured with familiar problems as chapter titles.
- I Don't Have Much Time and I Have To Change It
- I Can't Get This Class Into a Test Harness
- I Need to Make a Change. What Methods Should I Test?
- I Don't Understand the Code Well Enough to Change It
- How Do I Know I'm Not Breaking Anything?
With only a few minor exceptions, each chapter spoke directly to a problem I've personally encountered in one or more of the code bases I've worked on. Each gave me a specific set of techniques to apply to the problem in such a way that I could see how they would work. I've started to do just that in current project.
The last section of the book is a catalog of the simple but powerful dependency-breaking techniques introduced throughout. Of the two dozen presented, there are handful that form the core and that are used repeatedly throughout, while the rest are used only in particular, less-common situations. What struck me most is how much these techniques are about getting back to core design principles. Classes should have one responsibility. Methods should be short and do one thing. When you find code that violates these principles, you can use the refactorings in this book to introduce new methods and classes to break them up. Yes, it can take a long time to get to that idllyic land where everything is simple, clean, and under test. But a journey of a thousand miles begins with a single step, and if you take just one step a day every day, you'll get there sooner than you think.4
1 And by fast, he means that each test runs in 1/100th of a second. This means usually (1) no hitting the database, (2) no communicating over the network, (3) no touching the filesystem, and (4) no editing special configuration files to run it. Tests that do these things aren't bad, they just aren't unit tests.
2 While much of the book deals with object-oriented programs, because object seams are one of the easiest types to work with, in languages like C we have link seams and preprocessor seams as well.
3 At Agile 2010, I overheard Martin Fowler describe these as "blind refactorings," because you have to make what you hope are behavior-invariant changes without the benefit of tests.
4 And much sooner than if you never take a step.5
5 And even sooner if you don't take steps backward. Stop writing legacy code.