Tests are important. And Test-Driven-Development leads to well-tested, well-structured software. Of course, every programmer knows that nowadays. Unfortunately, this has not always been the case. And so, we are often faced with the challenge of working on a system without tests – a "legacy system" by Michael Feathers' definition.
But how do we proceed if we don't have test coverage to ensure that our changes don't break anything? And if, on the other hand, we can't write these tests because the code is too complex, contains far too many dependencies, and logic and presentation are intertwined?
What can we do if we can't instantiate the class under test because we have to pass an object to its constructor that we can't easily create? Or if the method under test accesses the database – even though that database access isn't necessary for the functionality we want to test?
This book contains a catalog of mechanisms to locate and break these dependencies. And to consequently modularize the code, creating module boundaries – so-called "seams" – where we can apply our unit tests.
The presented strategies allow you to make changes with minimal risk. Today, using the refactoring features of modern IDEs, you can perform many of these changes fully automated. These changes allow you to implement initial tests. And based on these, you can perform more far-reaching refactorings that ultimately transform the codebase into a better design.
The author explains all strategies using excellent, real-world examples, written primarily in Java and C++.
Unfortunately, the book has not been updated in 15 years. In particular, inexperienced programmers should take care not to implement every strategy dogmatically. For some mechanisms, there are better alternatives nowadays; some practices lead to worse, instead of better code; and the issue of thread safety – a core issue in most enterprise applications today – is entirely left out.
For example, these days, you don't need an interface for every class you want to mock. Mockito can easily mock classes, i.e., resolve dependencies without extracting an interface. And if you do extract an interface, please follow the Interface Segregation Principle! The author's statement, "it's nice to have an interface that covers all of the public methods of a class" (p. 366), is unfortunately in stark contradiction to this.
Invoking abstract methods from a constructor ("Extract and override factory method") is better avoided if you don't want to suddenly be faced with an object that is only partially initialized. It is for good reason that C++ forbids this practice. Fifty-five pages after the introduction of this strategy, the author finally advises against its use in Java.
Multithreading is not a topic in almost any of the older classics. Nowadays, no programmer can get around it. If you extract a class using strategies such as "Break Out Method Object", you should, therefore, add a hint that the class is not thread-safe. (Alternatively, you can protect instance variables by suitable lock mechanisms).
Despite the age-related weaknesses mentioned, I can recommend the book to any programmer who has to work with legacy code. Experienced programmers have certainly used one or the other strategy intuitively before. Other techniques may be new. Either way, the formalization helps to reinforce the strategies, and their names improve team communication.
Just keep two things in mind: the book is 15 years old, and there is an exception for every rule!
🎧 Suitable as an audiobook? No, due to numerous code examples.
* Disclosure: We love sharing our favorite books with you! As an Amazon Associate, we earn a small commission from purchases you make through our links, which helps us continue creating content you enjoy.