Books for software engineers and managers
How strongly do I recommend Working Effectively With Legacy Code?
7 / 10
Working Effectively With Legacy Code is basically about modifying code to increase testability. This book is very similar to Martin Fowler’s book Refactoring, but with more emphasis on testing than readability or flexibility.
If you feel stuck trying to make changes in a legacy codebase, the final ⅓ of this book is most relevant to you – it’s a collection of refactorings with small practical examples.
My only criticisms of this book are:
I skimmed some long code samples in the book because I already understood the concept, but engineers newer to refactoring might need to read more closely.
Top Ideas in This Book
Because changes cluster in systems, developers experience more benefit from improvements than if changes were more distributed throughout the system.
In other words, we are incentivized to improve the quality of legacy code because we are likely to return to that same spot in the near future.
Much of what makes code “legacy” ties back to untestable dependencies.
In my experience, developers who learn and apply dependency injection experience an epiphany in testability. Their code becomes more durable, delaying the time between original authorship and becoming legacy.
I’ve experienced this many times in complex codebases. Finding the point of entry and following the code through layers of abstraction may feel difficult. But once you find it, the change itself feels easy to make.
Similarly, adding tests for that change feels easy in a well maintained system.
Not having tests feels to me like the definition of legacy code, although the author doesn’t go that far.
Without tests, developers tread lightly and fear introducing regressions.
I find that the payoff period for adding tests to code is extremely short, on the order of hours. That is to say the time required to add tests is made up for with increased productivity within a matter of hours.
We perceive a tradeoff between code quality and development speed, but in practice their relationship is synergistic.
When adding tests to a legacy codebase, I often start with integration tests because only at that level can change be detected.
From there, I can start splitting out code in a way that supports unit tests. Now I have a second layer where change can be detected.
Test speed and localization go hand-in-hand. A test becomes faster when it runs against a narrower set of code.
In practice, tests suites creep toward slowness and eventually cross a threshold where developers stop adding to the test suite. As engineers, we’re responsible for keeping our test suite fast to ensure the ongoing its ongoing usage.
When we look across the office, we expect programmers to be writing code. Not staring at their screen reading.
That kind of social pressure can be unhelpful when working in legacy codebases which are inherently more difficult to understand.
Engineering managers can mitigate this risk by encouraging their engineers to spend time reading and diagramming the code.
Developers unaware of the system architecture are prone to introducing new patterns that ultimately degrade the existing architecture.
When new patterns emerge without automated testing, we end up with a hodge-podge system.
Programmers get excited about refactoring or adding new code and its tempting to start changing a bunch of things. When we change multiple things, it becomes harder to tell what changes are working vs. not.
A better technique is to change one thing at a time, in rapid succession.