Chapter 6: I Don’t Have Much Time and I Have to Change It
The everyday emergency: a feature is needed today, there are no tests, and you could just edit the ten places that need changing and be done by 5pm. The chapter is about the techniques that let you add a change without diving straight into that inline-edit trap — because, as Feathers puts it, code is your house and you have to live in it.
The first move is always to try instantiating the class in a test harness. If you can’t, Chapters 9 and 10 deal with that — and it’s often easier than you expect. If you genuinely can’t afford to break dependencies and get tests in place right now, the next question is: can you make the change as fresh, new code instead? Usually you can, and the techniques below show how. The caveat: new code you add this way is tested, but its use (the call sites) often isn’t, so use these carefully.
Sprout Method
When the new feature can be written entirely as new code, put it in a new method and call it from where it’s needed. You may not be able to get the call sites under test easily, but you can at least test the new method in isolation. It’s far better than adding code inline.
Use Sprout Method whenever the addition is a distinct piece of work, or when you can’t yet get tests around the host method.
If the class’s dependencies are so bad you can’t even instantiate it (without faking lots of constructor arguments), one option is Pass Null. When that won’t work, consider making the sprout a public static method, passing in whatever instance variables it needs as arguments. Statics make a useful staging area in legacy code: once several of them share variables, you can often see a new class waiting to be extracted; or, once the host class is under test, move them back in as instance methods.
Trade-offs of Sprout Method
Disadvantages
- You’re effectively giving up on the source method and class for now.
- The code is left in limbo — it isn’t getting appreciably better.
- The source method ends up in an odd state: a lot of complicated old code plus a single new sprout call.
Advantages
- A clear separation between new and old code.
- A clean interface between the two.
- You can see all the variables the new code touches, which makes it easier to judge whether the code is in the right place.
Sprout Class
Use this when there’s no realistic way to get the class into a test harness in reasonable time — too many creational or hidden dependencies — so you can’t even Sprout a Method on it. Instead, put your change in a new class and use it from the source class.
Two situations lead here:
- The change is really a new responsibility that doesn’t belong on the existing class anyway (e.g. a date check bolted onto a
TaxCalculatorwhose real job is calculating tax). - The earlier emergency case — a small piece of functionality that would fit an existing class, but you can’t get that class to even compile in a harness.
Steps:
- Identify where the change goes.
- If it can be written as a single sequence of statements in one place, name a class that could do that work, write code there to create an instance and call a method on it, then comment those lines out.
- Make the local variables the source method needs into constructor arguments.
- If the sprouted class must return values, give it a method that supplies them and add a call to receive them.
- Develop the sprout class test-first (TDD).
- Uncomment the creation and calls to wire it in.
Advantage: you move forward with far more confidence than invasive changes would allow. Disadvantage: conceptual complexity — you start gutting the existing abstractions and doing the real work elsewhere. Sometimes that’s right; sometimes you only do it because your back is against the wall, and things that ideally belong in one class end up scattered into sprouts just to make safe change possible.
Wrap Method
Adding behaviour straight into an existing method is easy but often wrong. A method usually starts out doing one thing; extra code bolted on later is suspicious — you’re probably adding it just because it has to run at the same time. That’s temporal coupling, and it’s nasty in excess: grouping things only because they happen together creates a weak relationship that’s hard to separate later when you need one without the other. Wrap Method (like Sprout Method) lets you add the behaviour without tangling it into the original.
Wrap Class
The class-level analogue, essentially the decorator pattern. The key idea: add new behaviour to a system without adding it to an existing class. When many calls need the new behaviour, a decorator-style wrapper lets you add it to all of them (e.g. every pay()) transparently and at once. When only a couple of call sites need it, a non-decorator wrapper works well. Over time, watch the wrapper’s responsibilities — it may deserve to become a first-class concept in the system.
Source
Working Effectively with Legacy Code, Michael Feathers — Chapter 6, I Don’t Have Much Time and I Have to Change It.