Chapter 7: It Takes Forever to Make a Change

Lag time

Lag time is the gap between making a change and getting real feedback about it.

The mind works differently depending on how long that gap is. If you can only act once a minute, you do a step, then wait — your attention drifts. Compress the gap from a minute to a few seconds and the quality of thought changes: work feels like driving rather than waiting at a bus stop, concentration is sharper, and mistakes get noticed and corrected far faster. Interpreted-language programmers often get this near-instant feedback for free. For the rest of us in compiled languages, the main obstacle is dependency — having to compile something we don’t care about just to compile the thing we do.

Breaking dependencies

Dependencies can be broken. In OO code the first step is usually trying to instantiate the class you need in a test harness. Once a class is there, you get fast edit-compile-link-test cycles. The trouble is that, in legacy systems, people get stopped at that very first step — the class is enormous, or buried under so many dependencies they overwhelm the part you actually want to change. When that happens, it pays to carve out a larger chunk of code and get the whole cluster under test.

Build dependencies

For a cluster of classes you want to build quickly, first find the dependencies that get in the way — again, just try to use the classes in a harness; almost every problem will be a dependency to break. After the cluster runs in the harness, look at everything that depends on it, since those will recompile whenever you rebuild.

The technique is to extract interfaces for the classes in the cluster that are used from outside it. Most IDEs can do this for you (pick the methods, name the interface, optionally rewrite references to point at the interface). Then move the cluster into its own package or library. The key insight:

When you break dependencies and section classes off into new packages or libraries, the total cost of rebuilding the whole system grows slightly — there are more files — but the average build time (a make based on what actually needs recompiling) can drop dramatically.

Worked example: AddOpportunityFormHandler

A small set of collaborating classes sits in one package. We want to change AddOpportunityFormHandler, but it depends on concrete classes — ConsultantSchedulerDB and AddOpportunityXMLGenerator — which may drag in many more. Instantiating it for a test could pull in who-knows-how-many classes.

  • The first dependency is ConsultantSchedulerDB, which connects to a database. Using Extract Implementer turns it into an interface (ConsultantSchedulerDB) with a separate ConsultantSchedulerDBImpl. Now the handler can be built with a fake implementing the interface. A side benefit: the handler no longer depends directly on ConsultantSchedulerDBImpl, so changes to the impl no longer force the handler to recompile.
  • Apply the same move to OpportunityItem, splitting out OpportunityItemImpl. Now AddOpportunityFormHandler depends on neither implementation — effectively a compilation firewall. Changes to the impl classes don’t force the handler (or its users) to recompile.
  • Make this explicit in the package layout: an OpportunityProcessing package with no dependency on the database implementation. Tests placed there compile fast and don’t rebuild when the DB implementation changes.

The Dependency Inversion Principle

When your code depends on an interface, the dependency is minor and unobtrusive: your code only changes if the interface changes, and interfaces change far less often than the code behind them. You can edit implementing classes or add new ones without touching users of the interface.

Prefer depending on interfaces or abstract classes over concrete classes. Depending on less volatile things minimises the chance that a change triggers massive recompilation.

Shielding users of the package too

The above stops the handler from recompiling when its dependencies change. The other half is stopping code that depends on the handler from recompiling. Since AddOpportunityFormHandler is the only public production class in its package, you can Extract Interface / Extract Implementer on it too, so other packages depend on the interface. That shields all users of the package from recompilation on most changes.

The trade-off

More interfaces and packages add some conceptual overhead, and the whole-system rebuild gets slightly longer. But getting a small cluster compiling separately and under test is a one-time cost, after which you reap fast feedback forever — fewer errors, less aggravation. Worth it.

Source

Working Effectively with Legacy Code, Michael Feathers — Chapter 7, It Takes Forever to Make a Change. For more on managing dependencies with interfaces and packages, Feathers points to Robert C. Martin’s Agile Software Development: Principles, Patterns, and Practices (Pearson, 2002).