“Working Effectively With Legacy Code”, My Reading Notes

Some Methods To Apply

Mock object

Legacy code usually doesn’t have a good unit test coverage, and turns out it is usually too tight-coupled to add unit tests. Thus, to test a target object, we can first add some mock objects to act like its dependent parts, then add tests for the target object.

Sprout method

Consider the case of adding a new feature into an existing function, which didn’t have unit test, and is too complicated to write unit test any soon.

existingFunc() { 
// ...
foreach (e : arr) {
// ...
// TODO: need a new feature
}
// ...
}

Embedding the new feature inside the structure of legacy code is usually the quickest way, but (1) it will be difficult to test the new feature, (2) the legacy function is growing even more complicated.

existingFunc() { 
// ...
foreach (e : arr) {
// ...
}
newFunc(arr);
// ...
}
newFunc(T[] arr) {
foreach (e : arr) {
// new feature
}
}

By putting the new feature into a new sprout function, we can (1) easily add unit test for the new feature, and (2) preserve readability of legacy function.

Wrap method

Consider the case of adding a new feature, which needs to be executed at the same time with a legacy function, but has a rather separate responsibility. Examples are: preprocessing input data, performance profiling, logging… etc.

funcA() { 
// ...
// TODO: need a new feature
}

By wrap function, we can (1) leave all the callers of the legacy function untouched (including the test programs, if any), and (2) easily add unit test for the new feature .

funcA() { 
orgFuncA();
newFuncX();
}
orgFuncA() {
// original funcA contents...
}
newFuncX() {
// new feature
}

Worth mentioning: the idea of wrap method is similar to Decorator pattern.

Extract method

When facing a long function, we can improve its readability by extracting small pieces from it, put into a new function and add unit test for the new function. One small step a time, we can gradually break down the giant.