skip to content

For a base type other teams extend, when is a self-referential parameter worth the cost it pushes onto every subtype?

level: principalimportance: should knowfreq 28%

answer

  1. a tax on every future subtype
  2. point at the operation that forces it
  3. count levels and extenders
  4. price restating operations per level
  5. write the rule beside the first use

basics

~20 s

Spend it only when an operation on the base must genuinely hand back or accept the most-derived type and the hierarchy is extended by people you will not review. Everywhere else the plain limit reads once, admits more types and gives up nothing callers relied on.

solid answer

~40 s

Treat it as a tax levied on every future subtype, not a local design choice. It is worth paying when the base's own operations must return or accept the exact implementing type - a chain that must not degrade, a comparison that must not cross families - and when the hierarchy is deep enough that the alternative of restating operations per level does not scale. Refuse it when there is one layer, when a plain at-or-below limit already lets the body do its work, or when the fluent surface is a convenience rather than a contract. The costs are concrete: every layer restates the parameter, bare uses of the type become awkward, and error messages print the expansion rather than the type the author meant.

go deeper

for a junior

Notice the cost before the cleverness: a signature that reads back on itself is harder for the next person. You are not expected to make this call, only to ask why the shape is there.

for a middle

Be able to state both sides. The parameter buys operations typed against the most-derived type; it charges every layer a parameter and every concrete type a hook.

for a senior

Argue from the population of extenders and from a named operation. Show that you have compared the parameter against restating operations per level and against moving the behaviour out of the hierarchy.

for a principal

Own the standard. Say when the team spends it, write the reason beside the first use, and be willing to decline it on a type where it would merely look rigorous.

## What you are actually deciding A self-referential parameter on a published base is not a local choice. It is a **requirement propagated to everyone who extends the type**, forever: each layer must decide whether to close the recursion or forward its placeholder, each concrete type owes the hook that produces the placeholder value, and every caller who merely wants to *hold* one of these values must decide what to write for the argument. The decision is therefore about the population of future extenders, not about elegance in the declaring file. ## The case for spending it The parameter earns its keep when the base's own operations must be typed against the most-derived type and there is no cheaper way to say so: - **A chain that must not degrade.** Shared configuration steps declared once on the base, called in any order with derived steps, in a hierarchy that is more than two levels deep. - **A comparison that must not cross families.** An operation whose partner must be of the implementing kind, where accepting the wrong kind produces a meaningless result rather than an obvious failure. - **A copy-with-change or merge** that must hand back the caller's own type, because forcing the type back is a cast the caller has no way to justify. - **Many extenders, no review.** If the type is published and you will not see the subtypes, a compile-time rule is the only rule that will actually be followed. ## The case for refusing it - The hierarchy is one level deep, or is extended only inside a team that can be told the convention. - The operation's callers are satisfied with the base as the result type - which is usually true when the chain ends in a build step anyway. - The platform lets an override narrow its return type and the number of shared operations is small, so restating them per level is cheaper than a parameter on every layer. - The behaviour can be moved out of the hierarchy entirely: an explicit comparison object beside the values, or a separate configuration record that the builder consumes. This removes the shape rather than taming it, and it handles the case where more than one ordering or more than one configuration is meaningful. ## The bill, stated plainly | Cost | Who pays it | How it shows up | |---|---|---| | Parameter on every layer | Every extender | Intermediate types must stay parameterized; closing early freezes the chain | | The hook producing the placeholder value | Every concrete type | One-line boilerplate, silently wrong if forgotten at a level | | Awkward bare uses | Every caller storing the type | A holder or field declaration has no obvious argument to write | | Long diagnostics | Everyone debugging | Messages print the expanded limit rather than the intended type | | Reading cost | Every reviewer | The declaration reads back on itself and needs a stated reason beside it | ## How to decide, concretely 1. **Name the operation that forces it.** If you cannot point at one operation whose signature must mention the most-derived type, the answer is no. "It felt more type-safe" is not an operation. 2. **Count the levels and the extenders.** One level and a handful of known extenders means a convention, a review note and a test do the job. Deep and public means the type system should carry it. 3. **Price the alternative.** Restating operations per level costs operations times levels, once, in code you control. The parameter costs a small, permanent obligation in code you do not. 4. **Decide what happens when it is not honoured.** If a forgotten hook or an early closure degrades quietly, add the test that catches it; the shape does not protect against being used wrong, only against being used with the wrong type. ## Standard-setting, not cleverness The failure mode a lead should watch for is the shape spreading by imitation: one justified use in a core base type, then the same parameter appearing on every builder in the codebase because it looked like the house style. Write down the rule with the first use - *this parameter exists so that these named operations return the caller's own type; do not add it to a type that has no such operation* - and the rest of the codebase stays readable. The judgment being tested here is not whether you can write the signature; it is whether you can decline to.

  • What is the cheapest alternative that removes the shape entirely?
    Move the behaviour out of the hierarchy. Pass an explicit comparison object beside the values instead of requiring them to be self-comparable, or have the builder consume a separate configuration record. Both give up the fluent or self-typed surface and gain a type nobody has to be taught, and they also handle the case where several orderings or configurations are meaningful at once.
  • How would you stop the shape spreading by imitation across a codebase?
    Write the rule beside its first use: the parameter exists so that these specific operations return the caller's own type, and it does not belong on a type with no such operation. Pair it with a review note. Without a stated reason, the signature reads as house style and reappears on every builder, where it costs the same and buys nothing.
  • What would change your answer from refuse to spend?
    Evidence that extenders are getting it wrong in a way you cannot see: casts appearing in callers' code to recover a type, bug reports from mixed comparisons, or a hierarchy that has grown past two levels in the wild. A compile-time rule is worth its cost exactly when a convention has already failed with people who do not attend your reviews.

saying these in an interview costs you the question

  • Treats the parameter as free because the declaring file compiles
  • Adds it to every type with a fluent surface as house style
  • Cannot name the operation whose signature requires the most-derived type
  • Assumes the shape also prevents a subtype being used incorrectly
  • Dismisses the per-level alternative without counting operations and levels