Taming The Destroyer Of Software – Part 2
We’ve been taking a closer look into Unnecessary Complexity (UC), the crucial determinant of why ageing software becomes increasingly difficult to modify. Yesterday we discovered that Test-Driven Development (TDD) is an effective technique for eliminating UC when writing new code.
However, what do we do when we already have a codebase affected by UC entropy?
When in a hole, stop digging. When affected by messy, high-UC Legacy Code, stop making it worse.
Unfortunately, high-UC code tends to rot quickly. Something needs to change. And before the code does, it’s us that needs to change. We need to learn and adopt new techniques for managing Legacy Code.
In his excellent book “Working Effectively With Legacy Code“, Michael Feathers introduces us to the crucial tools we need: Stabilise existing Legacy Code with unit tests to prevent us from breaking expected behaviour as we make changes.
Simple but not easy. In statically typed languages, like C++ or Java, wrapping unit tests around Legacy Code can be tricky. We may have little idea what the code does. However, the greatest obstacle to the introduction of unit tests are concrete dependencies. Before we can introduce unit tests, we must break these dependencies.
For example, the following line of code sits inside some horrible C# Legacy Code. To stabilise the current behaviour, we would like to establish unit tests.
var dataAccess = new SqlServerDataAccess(config);
This concrete dependency on
SqlServerDataAccess is stopping us from writing a unit test. Why? We do not want our unit test to connect to the database! That would be an integration test rather than a unit test. Integration tests are fragile and slow. We would rather have a unit test here. OK, how do we break the local dependency on
SqlServerDataAccess? Well, it depends on the particular Legacy Code. One way may be to use an interface or other abstraction instead of the concrete SqlServerDataAccess class. The Dependency Inversion Principle could prove useful.
Once we have unit tests in place, we can then make more invasive modification, e.g. refactoring, knowing that the unit tests will let us know if we have broken existing behaviour.
“Working Effectively with Legacy Code” is a treasure trove of dependency breaking techniques. The book points out how to identify great places to place unit tests in Legacy Code, what kind of unit tests to write (Spoiler: We are more interested in specifying existing behaviour and not necessarily correctness). A great resource that every developer who works with Legacy Code (and who doesn’t?) should have on their bookshelf.
Martin Fowler’s “Refactoring” is also an excellent resource.
Tomorrow we’ll wrap up our journey into Unnecessary Complexity.