The short recommendation: Read this book. Even if you aren't working with existing code that needs to be updated, the techniques can be used to recognize potential issues on new code that you're writing. And if you are trying to untangle an existing code base, this book is invaluable.
Refactoring for Testability
The best way to describe this book is "refactoring for testability". The idea behind this is that if you need to make modifications to code, you need to first have tests in place. If you do not have tests for the existing code, then if your changes break the current functionality, you won't know about it. With tests in place, you will know if you break something as soon as you make those changes.
This is a "Do no harm" approach. The code that you are walking up to is working code (you're adding features, not necessarily fixing bugs). The last thing you want to do is to remove the "working" part.
This is especially critical if you are working with code that you didn't write yourself (or even code you did write yourself several years ago). You need to recognize that if you are going to be successful in working with unfamiliar code, you need to make small changes one step at a time.
Three Parts
The book is broken down into 3 parts.
Part I: The Mechanics of Change
Part one focuses on why we need to make changes to code, how unit testing can help us make confident changes, and several tools and techniques (such as finding/creating "seams", refactoring tools, and mocking). This is a high-level overview of why we need to start working this way (refactoring for testability).
Part II: Changing Software
Part two focuses on specific problems that you may encounter in the code -- things that make the code hard to get into a test harness. Personally, I just like reading the chapter titles. Here are a few:
- I Don't Have Much Time and I Have to Change It
- I Need to Make a Change, but I Don't Know What Tests to Write
- Dependencies on Libraries Are Killing Me
- I Don't Understand the Code Well Enough to Change It
- My Application Has No Structure
- My Test Code Is in the Way
- My Project Is Not Object Oriented. How Do I Make Safe Changes?
- This Class Is Too Big and I Don't Want It to Get Any Bigger
- I Need to Change a Monster Method and I Can't Write Tests for It
- How Do I Know That I'm Not Breaking Anything?
Part three consists of a catalog of 24 techniques that you can use to bring code under test. These are all presented in a short (usually 3-5 page) format with an example and specific steps to follow. These techniques are referenced in Part II, so you'll probably be familiar with the concepts.
Some examples:
- Break Out Method Object
- Encapsulate Global References
- Expose Static Method
- Extract Implementer
- Extract Interface
- Parameterize Constructor
- Pull Up Feature
- Push Down Dependency
- Subclass and Override Method
An Example
One chapter that especially jumped out at me (not sure why -- probably because I've seen this before) is "I'm Changing the Same Code All Over the Place." This has to do with reducing the duplication of code between 2 classes.
What I really liked about this chapter is that it shows the reduction in duplication is small steps. There is no, "just extract everything to a superclass and you'll be fine." Instead, it starts by figuring out what the commonalities are between the classes -- starting with properties and methods.
The first step was to create a superclass with *one* common method. Then the implementation methods (in each class) are updated to make them more similar (by extracting out a method to make the body of the common method identical). Then this implementation can be moved up to the superclass with only the differences (the extracted method) in each subclass.
The next step was to move the common properties up to the superclass. Then the refactoring continues one step at a time. When we are left with some unique properties in each class, the question is can we combine these into a common collection.
I don't expect that this description will make much sense out of context. The point is that instead of thinking about the refactoring in big chunks, we are thinking about it in very small steps. At each step, we can make sure that the tests still work as expected. And if a test fails, we can immediately know what happened. If we were to do a "big blast" change and the tests fail, we won't know specifically what the problem is.
As someone who has rolled back big chunks of changes (because it didn't work and I wasn't quite sure why), I really appreciate this approach.
Wrap Up
To wrap things up, whether you need to modify an existing code base or you are just looking at techniques to make your code more testable, Working Effectively with Legacy Code is an excellent read. Note: none of the examples are in C#, but they are in C-family languages (C, C++, and Java), but C# is mentioned in many of the techniques. (There's also one Ruby example for a technique that deals with dynamic languages.) If you are comfortable reading C#, you should be able to understand the examples just fine.
I'm always looking to improve my coding techniques, and I'm sure that I will be thumbing through this book many times in the coming years.
Happy Coding!