Module 15 — Debugging¶
The technique this module adds¶
Reading code for changeability: the change-cost trace, and the smell catalogue.
Every previous module's debugging started from a symptom — a wrong answer, a crash, a leak, a slow query. This module starts from something that has no symptom at all: a design that works, is correct, and is expensive to change. There is nothing to observe, no failing test and no message, because a design problem is not a defect in the program's behaviour. It is a defect in the next change.
So the diagnosis is performed on a requirement that does not exist yet.
Move 1 — pick a requirement and count what it touches¶
The instrument is a hypothetical, chosen to be plausible, and the measurement is a count:
"Fines now depend on the member's category as well as the item's kind."
Trace it through the code and write down every file, class and function the change would have to touch. In the Lab 14 system, that is:
the fine rule itself (the arithmetic)
every place that checks a limit (because the limit is the same kind of rule)
the report that formats a total (because it calls the fine rule to show it)
the tests that assert on a total (because the number changes)
the record loader (because the category must be read from somewhere)
count: 5 sites in 4 files
A count above two or three is a design finding. Nothing is broken; the change is simply not local, and that is what the principle names:
| The count includes | The smell | The principle it violates |
|---|---|---|
| The same class, three unrelated reasons | God class | Single responsibility |
| The same switch, in several functions | Growing conditional | Open/closed |
| A subclass's behaviour breaking a caller | Impossible substitution | Liskov substitution |
| Classes that break when an unrelated interface changes | Fat interface | Interface segregation |
| A class that constructs what it depends on | Hidden dependency | Dependency inversion |
The trace is also the evidence that a refactoring worked: run it again afterwards and compare the numbers.
Move 2 — read the class diagram for trouble¶
A diagram drawn from your code answers questions a code listing does not:
- Which class has the most arrows into it? That is where a change ripples from.
- Which interface has the most implementers? That is where a new operation costs the most, and where the compiler will list every broken implementer:
error: cannot declare variable 'catalogue' to be of abstract type 'ReadOnlyCatalogue'
note: because the following virtual functions are pure within 'ReadOnlyCatalogue':
note: 'virtual void LibraryService::printReceipt(const std::string&)'
- Which arrows point at concrete types? Those are the dependencies that cannot be doubled in a test:
Desk ┈┈> ConcreteEmailer dependency on a concrete class: untestable
Desk ┈┈> Notifier dependency on an interface the Desk owns: testable
- Which arrows are the wrong way round? A policy whose header must include its implementation's header has not been inverted, whatever the design document says.
Move 3 — state the responsibility in one sentence¶
The cheapest design instrument in this module needs no tool:
Read it back. Does it need "and"? Do the two clauses have the same audience, and the same rate of change? If they do, the split is noise; if they do not, the sentence has just told you where the seam is. This is also the fastest way to check a proposed split: write the two new sentences, and see whether each of them is something you would say to a colleague.
Move 4 — look for the variation that has not been named¶
A design problem in this module is almost always a variation that exists in the code but has no name:
// the variation is the KIND, and it is spelled as data in three places
if (kind == "book") { days = 21; }
else if (kind == "dvd") { days = 7; }
Ask: which of these things has more than one shape, and is that shape a string, an enum, an if or a
dynamic_cast? Every one of those is a place behaviour is being chosen from data, and the refactoring
is the same each time: make the thing a type, and let it answer for itself.
The counterpart question, which matters just as much: is this variation real? If there is one kind
today and no requirement for a second, the if is the honest design, and the polymorphic version is
speculation paid for in types (section 13 of the concepts page).
Move 5 — measure the reader cost, not just the writer cost¶
Design discussions focus on the person making the change. The larger cost is usually the person reading the result, and it can be estimated in lines and hops:
to answer 'what is the fine for a staff DVD?', version A needs 4 lines of
arithmetic; version B needs the reader to follow a registry, a factory, an
id string built by concatenation, and a class per combination.
Both versions are correct. Version B makes one kind of change easier and every question harder, and that trade is the one to state in the design note. When the two costs are counted, "more abstraction is better" stops being a default.
The escalation ladder for this module¶
- Write the requirement down. Design work without a stated requirement produces preferences.
- Count what it touches. Files, classes, functions — a number, not an impression.
- Name the smell from the table in Move 1, and check the common mistakes table for the mechanism.
- State the responsibility sentence of the class the change lands in. Most findings surface here.
- Follow the arrows. Which dependency points at a concrete type, and whose header includes whose?
- Refactor, then re-run the count, and compare the two numbers. That comparison is the only evidence that the design improved rather than merely changed.
Note what is deliberately absent: there is no step that says "add a pattern". A pattern is a name for the shape the refactoring lands in, and reaching for the name first is the mistake this module's last example exists to correct.
Checklist before calling a design finished¶
- [ ] Every class's responsibility is one sentence, without "and"
- [ ] The change-cost trace has been run for the two most plausible next requirements
- [ ] No interface forces an implementer to write a stub
- [ ] Every interface has a virtual destructor
- [ ] No derived type throws from, or cannot honour, an inherited operation
- [ ] Every policy owns the interface it depends on, and can be given a test double
- [ ] Every pattern in the design names the requirement that justified it
- [ ] Every pattern considered and not used has a written rejection, with its reason
- [ ] The design report names the cost of each choice, and one decision I would reverse
Continue¶
Previous: Misconceptions and mistakes · Next: Exercises · Module home: Module 15