skip to content

Why does a four-level base-class chain stop an automated case from declaring the setup it needs?

level: middleimportance: should knowfreq 54%

answer

  1. Requirements are not a tree
  2. Ancestry chooses, the case cannot
  3. Subsets of steps, not a path
  4. Guards in the ancestor concede the point
  5. Name the units at the case

basics

~20 s

Depth converts a requirement into a location: what a case gets is decided by where it sits in the chain, not by anything it states. Composition returns the declaration — the case names the units it uses.

solid answer

~40 s

In a chain, a case's preparation is chosen by its ancestry. A case that needs a signed-in user but no seeded data has no way to say so: the only lever is which parent it extends, and every new combination of requirements demands a new branch. Preparation needs are not hierarchical — they are a set, and cases want arbitrary subsets — so the tree either explodes or grows guards in the ancestor that descendants set, which is the hierarchy conceding the step was never universal. Composition inverts it: small preparation units exist independently and the case names the two it needs at the top of its body. Requirements become readable where the case is read, subsets cost nothing, and each unit can change without disturbing anything that did not ask for it.

code

pseudocode · 14 lines
pseudocode
# ancestry decides
class Base:                    prepare(): start_capture()
class Authed extends Base:     prepare(): parent_prepare(); sign_in(user)
class Seeded extends Authed:   prepare(): parent_prepare(); seed_account()

class PriceRounding extends Seeded:
    # wanted capture and seeding, did not want sign-in: no way to say so

# the case declares
class PriceRounding:
    run():
        capture = capture_unit.start()
        account = account_unit.seed(plan = "basic")
        assert_price_rounds(account)

go deeper

for a junior

Know what your case inherits and from how far up. Be able to say which preparation steps your case actually depends on, and notice when the only way to change them is to extend something different.

for a middle

Explain why preparation needs form a set rather than a tree, so a chain expresses only one path through them, and describe how naming small units at the case restores the declaration a chain removed.

for a senior

Expect to be asked what you do once the hierarchy has already exploded. Recognise guards and skip-only subclasses as the hierarchy conceding, and argue the change in terms of readability and blast radius rather than taste.

for a principal

Own the rule the team applies to new preparation code and the cost argument behind it: implicit acquisition multiplies the cost of every future change by the size of the subtree beneath the step.

## Acquisition versus declaration Two things can be true of a case's preparation. It can be **acquired** — the case receives it because of where it sits in a class chain — or it can be **declared** — the case names it. Inheritance offers only the first. A case that extends a parent takes everything that parent and its own ancestors carry, and it takes all of it, in whatever combination the chain happens to define. Nothing in the case records what arrived, and nothing in the case can change it. At one level of depth that is tolerable: the reader opens one class and sees the whole story. At four levels it stops being tolerable, because the effective preparation of a case is the union of four class bodies, each written by a different person at a different time, and none of them next to the case. ## Preparation needs are a set, not a tree The deeper problem is a shape mismatch. Suppose a suite has five independent preparation concerns: artefact capture, a signed-in identity, seeded account data, a feature toggle, and a stand-in for a downstream dependency. Cases want **subsets** of those five — and five concerns have thirty-two subsets. A class chain can express only **paths**: each level adds to everything below it, and a case picks exactly one position on one path. That leaves a team three moves, all of them bad: 1. **Branch the hierarchy.** Add an intermediate parent for each combination anyone needs. The class count tracks the combination count, and every new concern potentially doubles it. 2. **Raise everything.** Put all five in the topmost ancestor so every case gets all five. The requirement is now uniform and wrong: cases pay for concerns they never touch, and the whole subtree fails when any one concern breaks. 3. **Add a guard.** Leave the step in the ancestor but wrap it in a value descendants set. This is the hierarchy conceding in public that the step is not universal — and the guard's default is invisible at the case, so a new case gets whatever the last person decided. The third move is the most common because it is the smallest commit, and it ages worst: guards accumulate, interact, and eventually nobody can say what a given case prepares without running it. ## What composition gives back Composition here means the preparation concerns exist as **small independent units**, one per concern, and the case invokes the ones it wants: - **The requirement becomes readable where the case is read.** Three lines at the top of the case are the complete answer to "what does this need?" - **Subsets cost nothing.** Wanting capture and seeding but not sign-in is three words, not a new branch of the tree. - **Each unit changes alone.** Altering how identities are established touches the cases that asked for identities and nothing else. - **The blast radius shrinks to the callers.** A broken unit fails the cases that named it, and the failure points at the unit rather than at the case. - **Order is stated rather than inferred.** Because the case invokes the units in written order, nobody has to reconstruct a sequence from class positions. | | Chain of ancestors | Named units the case invokes | |---|---|---| | How preparation is chosen | by position in the chain | by the case naming it | | Expressing a subset | needs a new branch or a guard | invoke fewer units | | Where you read it | across every level | at the top of the case | | Effect of changing one concern | everything beneath it | only the cases that named it | | Cost of an unwanted step | paid silently | never incurred | ## The honest cost, and where the line sits Composition is not free. The case gains a few lines of ceremony, and if two hundred cases genuinely need the same four units, those four lines are repeated two hundred times. Two responses to that are reasonable and one is not: - **Reasonable:** give the recurring group one named composed unit that starts the four, invoked explicitly. The list stays visible at the case, and a case wanting three of the four can still say so. - **Reasonable:** keep a single thin ancestor for genuinely invariant, unconditional work that every descendant needs and no descendant could decline — and nothing else. - **Not reasonable:** reintroduce the chain because the repetition looked untidy. Repetition that is readable is cheaper than concealment that is not; the cost of a chain was never the typing, it is that nobody can tell what a case requires. The signal that settles most arguments is the **opt-out question**: could any case plausibly want this step off? If yes, no depth of hierarchy expresses it honestly, and the step belongs somewhere a case can decline it by simply not naming it.

  • Cases now list four units each and the lists look repetitive. Is that a step backwards?
    Repetition that is readable is usually cheaper than concealment that is not. If a real group of cases needs the same four every time, give that group one named composed unit that starts the four, rather than a new ancestor. The list stays visible at the case, and a case that wants three of the four can still say so without inventing a class.
  • How would you decide whether a preparation concern belongs in an ancestor or in a named unit?
    Ask whether any descendant could ever want it off. If the answer is yes — now or plausibly later — it is a unit, because inheritance offers no opt-out that is not a workaround. If the answer is genuinely never, and the step takes no value the case chooses, an ancestor is defensible. A chosen value is the clearest tell that a step is case-specific.

Preparation needs are a menu rather than a set meal, and a chain can only serve you every course in order up to the seat you were given.

saying these in an interview costs you the question

  • Says a deep chain is fine as long as it is documented
  • Answers every reuse question with a new intermediate parent class
  • Adds a guard to the ancestor so descendants can skip a step
  • Believes composition means duplicating preparation code in every case
  • Cannot name what their own case requires without opening the parents