Friday, November 3, 2017

Sustainability: Refactoring, Design, and Testing

I recently saw a Twitter post by Mark Seemann preferring the term "sustainable code" over "maintainable code." As it happens I had also been having some conversations about TDD (Test Driven Design) at work. Most people, when they think of unit testing, think the purpose is to reduce code errors. To be sure, that is an important aspect of unit testing and writing tests first, I think, has led to me making fewer errors and introducing fewer bugs than I would otherwise. I find this out every time I prototype something without tests then convert it into production code by introducing tests after the fact. (Yes, I'm not a TDD purist) Invariably as I start writing tests I find cases that I hadn't thought about and even - gasp - errors that I didn't even recognize but that were obvious after having written a test.

The concept of "sustainable code" is really why I've settled on TDD as a core practice. I should say TDD and refactoring because I believe the two go hand in hand - at least for me, and if my experience with other developers I work with over time is an indication of general trends, likely you as well.

In my experience, if you don't write tests first, you don't write tests - or at least many tests, the number of tests that it would take to allow you to refactor safely and confidently. In my internal conversations I talked about "refactoring with abandon" but it's not as undirected as that implies, but certainly you will have a lot less fear of breaking things and will engage in refactorings that would otherwise seem foolish indeed. I have both written test-first and test-last. Writing tests last feels like an onerous chore that you have to force yourself to do. It's easy to write just the happy path tests - and since you've already written the code, you know how to write a test that makes that code pass.  When I write tests first, it's part of design. I'm thinking, not of how to write a passing test, but how to make the code do all the things I think it should do. It's a creative activity not a bookkeeping activity. It makes a HUGE difference in the joy I get from writing that code.

Refactoring, continually improving code as you're creating new features, is the key to sustainable code. I'll note here that "refactoring" is not "changing what the code does" but rather "how the code does it."  In other words, if you've written tests, then those tests shouldn't need to change as a result of a refactoring. If they do, then you've over-specified in your tests. That sometimes happens, but usually you can easily tell whether a refactoring has broken behavior or broken a specification that was too prescriptive.  In that way refactoring can improve both your tests AND your code.

The key to being able to refactor with confidence is having a set of tests that will tell you when what you have changed breaks something in an unintended way.  If I make a change and a test starts failing, and that failure isn't merely an over-specification, then that change isn't an improvement, it's a bug.  I need to go back and continue making the improvement to account for the behavior I've broken.  This is especially true once the code has gotten complicated enough that you can no longer keep an accurate model of how things work in your head. At that point you begin to rely more heavily on your tests as a safety net; they're the embodiment of the working knowledge you had about that part of the code at the time that you most understood it.

Once you're able to refactor with confidence, you have a platform upon which you can build a system sustainably.  When you see inevitably see a piece of code that you now know is crappy, given your increased understanding of programming or changes in technology that allow a better solution, you can change it with confidence, knowing that the code still does what it was intended to do.  When you add a new feature to something that is a dependency for something else, you can be sure that you're not breaking anything upstream that relies on it because you have tests to ensure that you're not breaking existing behavior.

For me the big win with TDD is sustainability. You can continue to improve and extend the useful life of your software without the expense of a ground-up rewrite. There are huge cost savings over the lifetime of your software that result from using TDD and refactoring. Fewer bugs is a really, really nice side-effect, but it's not the primary reason I do TDD any more. It's about keeping the codebase clean and easy to work in, and easier to innovate on and add new features that drive revenue or customer satisfaction.