What is your view of unit tests and test-driven development (TDD)? If you believe that these techniques are merely a passing fad, or largely a waste of time, then read no further, and good luck with that approach! However, if you want to understand how TDD can be a powerful weapon for attacking the 'monsters' of ageing and not-tested code, or want to ensure that new code you write is maintainable, or indeed you (like me) are concerned about how we can effectively improve and develop legacy systems in the context of infrastructure-as-code and end-to-end automation, then Working Effectively with Legacy Code is essential reading.
The reason why we left behind the TDD skeptics (and they still exist, even as I write this in 2012) is that Michael Feathers is very clear about the nature of legacy code: legacy code has no test coverage, and 'code without tests is bad code'. Let's consider the implication of this view: even if your code was written yesterday, if there are no unit tests to characterise or define the behaviour of the code, then the code is legacy, and will be difficult to change without introducing bugs. On this basis, it becomes dangerous, disrespectful to others and perhaps even unprofessional to write code without tests.
We are also urged to expand our view of why we write tests, especially when working with legacy code: instead of specifying correctness, tests can be written to characterise behaviour of code. This is particularly helpful when we are breaking dependencies, as using tests to characterise behaviour helps to build an understanding of how non-tested code actually operates.
Michael Feathers uses the real-world image of a workbench clamp to show how tests can be used to hold tightly or 'clamp' the existing behaviour under test, just as a component in a workshop would be clamped when being worked on to avoid unexpected movement or behaviour during testing. We want to clamp the part of the code under test to enable us to work safely with it without unintended side effects.
Structure of the Book
The book has three main sections. In the first section, The Mechanics of Change, Michael asks us to consider why we want to make changes to software: is the change to fix a bug, to implement a new feature, to improve the use of resources (memory, disk, CPU), or to improve the design via refactoring? Crucially, a change set committed to version control should contain only one of these change types, not a mixture; the change set will thus have a single responsibility, and make future branching and merging easier.
The first section also introduces concepts such as 'seams' (alter behaviour without editing in that place) and 'enabling points' (the place where you choose a particular behaviour) which struck me as very effective mental constructs with which to approach any code, not just legacy code.
The second part of the book, Changing Software, consists of a set of scenarios in which you might find yourself when confronted with a codebase without tests; there are chapters such as 'My Application Has No Structure' and 'This Class is Too Big...' Use this part of Legacy Code when you have already characterised the situation you face, and want to go directly to a solution.
The third and final section, Dependency-Breaking Techniques, covers much of the same ground as part two, but from a different perspective: it is organised as a 'cookbook' of 'recipes', and the entries here together form a kind of toolbox for working with legacy code, allowing you to `dip in' to particular solutions without needing to have read the earlier sections.
Some reviewers have noted that Legacy Code covers only those languages (C++, Java, C#, etc.) which have xUnit frameworks available, and can therefore be covered by unit tests; other 'testless', legacy code written in VB, Pascal, embedded C, COBOL, etc., is left 'out in the cold' by the book, as these languages generally cannot have unit tests retro-fitted. To my mind, this view somewhat misses the point. Feathers is clear to associate legacy code with an absence of tests, irrespective of the language.
To work effectively with legacy code, we need to bring it under test; other techniques do exist, but are likely to be unsafe and error-prone when compared to a test-driven approach.