Koterpillar

Review: Working Effectively with Legacy Code

Thanks to the library of the company I work in for the ability to request books. "Working Effectively" is more than 10 years old, but the title looked promising.

In essense, all the book advocates is divide (the code into separate pieces), write tests and conquer (make changes). Perhaps about 70% is devoted to the ways of how to achieve that, with examples in Java. So knowledge of Java or at least the OO paradigm is a must before reading (and C and C++ desirable).

Of course, in the time being the programming languages and tools have changed, and it was interesting to compare whether the described techniques are now simpler, obsolete or new ones are needed.

On one hand, Java, being a statically typed language with rather strict decisions, makes one resort to building a complex web of interfaces, overridden methods and objects in the hard cases. To my knowledge, it now has tools that allow circumventing the inheritance, method visibility and other restrictions when testing. But it isn't worthwile in all cases - improving the code structure, even just to run the tests, is beneficial for changes later.

I won't discuss the C++ examples at all, because all they are is cheating on the compiler and linker. A lot of places use macros - I wonder if the author ever got a project where the macros themselves needed testing? However, good to read for the general knowledge.

The place where I envy the author is reliance on the compile stage errors. Thanks to the strict typing, practically the only place where a runtime error is possible is the implicit float to int conversion, and this is pointed out several times in different chapters. On the other hand, the author is heavily exploiting null, suggesting passing if an object of the needed type can't be constructed and hope it works.

There are no higher order functions in the book - perhaps Java didn't have delegates at the time. In C++, of course, one can make callable objects and even custom method tables, but, again, doing it just for testing is an overkill.

An interesting technique I saw that doesn't depend on particular language or paradigm is "scratch refactoring". Having saved the current state in a version control system, you start changing everything you can, however you want, paying no attention to the tests. If the changes touch several areas, that's still fine, continue as you please. And when there is finally a satisfactory result... remove all the changes, revert to the saved version and start again, with tests. The refactoring itself won't advance very far, but the experiment will give you an understanding of the direction to take.

One more note is about the architecture. Very often its understanding is different between team members, especially if the project has an architect detached from the real life. For this not to happen, the author suggests periodically explaining the system or its components' functionality, assuming no prior knowledge, in a limited number of sentences. Thus all the participants get an idealized version of the architecture, and subconsiously strive to support and enhance it instead of making random changes. I wonder whether these idealized descriptions can go into the modules' documentation, or it is worthwile making the team members make them up from scratch every time?

The last thing I want to point out is that the author focuses on writing unit tests. There is a good criterion for them - they are the tests that can run faster than 0.1 seconds. This resolves the contradiction in the "testing in isolation" definition - it's obvious that the + operator, when it is used in the tested method, doesn't need a mock, but an external service call, like a database, and similar must be excluded. It is a pity that Django, for example, practically imposes the reverse approach, where there are extensive tools for using ORM when testing and almost none for replacing it.

General recommendation: for working with object-oriented code, worth a read. Techniques specific to a particular language can be leafed through.