skip to content

Which calls does a stand-in that intercepts by overriding members silently miss, even when every caller goes through it?

level: middleimportance: must knowfreq 50%

answer

  1. no dispatch point, no interception
  2. overriding needs something to override
  3. a field read is not a call
  4. resolved from the declared type, not the object
  5. construction happens before the wrapping

basics

~20 s

Anything that is not a dynamically dispatched call on the instance: members no subtype may replace, members resolved from the declared type or with no receiver at all, direct field reads, and work the target did while it was being constructed.

solid answer

~40 s

Interception by overriding needs a **dispatch point**, a moment where the runtime chooses an implementation based on the actual object. Where there is no such moment, there is nothing to stand in front of. A member declared so that no subtype may replace it binds straight to the original body. A member resolved against the declared type, or one belonging to the type rather than an instance, never consults the object at all. Reading a field is not a call and has no dispatch point. And whatever the target did while being constructed happened before anything wrapped it. In every case the caller sees ordinary, working code, and only the added behaviour is missing.

code

pseudocode · 10 lines
pseudocode
object Ledger:
    constructor(source):
        total = recompute(source)   // runs while the object is being built
    method recompute(source): ...
    method report(): ...

raw = Ledger(source)                // recompute() has already run
guarded = AuditStandIn(raw)         // the stand-in exists only now
guarded.report()                    // audited
guarded.recompute(source)           // audited from here on, but not the first run

go deeper

for a junior

Recall that a stand-in can only act where a call actually looks up an implementation on the object, so a field read or a fixed member gives it no opening.

for a middle

Explain dispatch as the seam: name the constructs that resolve without consulting the object, and say why each one leaves the added behaviour out.

for a senior

Diagnose from an absence rather than an error, and redesign so the concerns you depend on sit at boundaries that genuinely dispatch.

for a principal

Set the rule for which member kinds may carry declared cross-cutting behaviour, so no team declares something the structure cannot deliver.

## Interception by overriding needs something to override A stand-in that works by replacing members relies on **dynamic dispatch**: the call names a member, and the runtime picks the implementation from the object actually receiving it. Because the stand-in is the object being received, its implementation wins, and that is where the added behaviour runs. Everything follows from the contrapositive. Where the implementation is chosen by something other than the receiving object, or where no call happens at all, the stand-in has no point of entry. It is not that interception was attempted and lost; there was never a moment at which it could occur. ## The constructs with no dispatch point - **A member declared so that no subtype may replace it.** The decision is fixed at the declaration; a stand-in inherits the body but cannot substitute its own, so the call runs the original code with nothing around it. - **A member resolved from the declared type of the reference rather than the runtime type of the object.** Some languages dispatch dynamically by default and some only where the declaration opts in; where it is resolved statically, the compiler has already chosen before any object exists. - **A member belonging to the type rather than to an instance.** The call names no receiver, so there is nothing to substitute. Behaviour that must cover it has to come from somewhere other than standing in front of an object. - **A direct field read or write.** This is not a call. There is no member lookup, no dispatch, and therefore no seam. - **A member a subtype is not permitted to see.** If visibility rules hide it from a subtype, that subtype cannot replace it, whatever else it can do. - **Work performed while the target was being constructed.** A wrapper is placed around an object that already exists, so construction precedes the stand-in. Anything decided, cached or emitted during construction happened when there was nothing to intercept it. | construct | why the stand-in cannot reach it | what to do instead | |---|---|---| | non-replaceable member | the implementation is fixed at declaration | make it replaceable, or move the concern to a caller-side boundary | | resolved from the declared type | chosen before any object is involved | route the call through a contract that dispatches dynamically | | member with no receiver | nothing to substitute | put the concern in the member's own code | | field read | no call, so no dispatch | expose it through a member if it must be observed | | construction-time work | the stand-in does not exist yet | do the work lazily on a call, or cover it at the factory | ## The symptom is the same every time None of these throws. The program runs, returns the right value, and simply lacks the caching, timing, retry or audit step the declaration led everyone to expect. That is what makes this material a diagnostic skill rather than a definition: the evidence you get is an **absence**, so you have to reason from structure instead of from a failure. There is a second-order surprise worth knowing. Where the stand-in is a distinct object standing in front of the target, a member it could not replace still runs the original body against the original state, which is usually what you wanted minus the interception. Where the stand-in is a fresh object of a derived shape rather than a wrapper around an existing one, a member it could not replace can end up running against **that** object's state rather than the target's, which is a correctness problem, not just a missing feature. How the stand-in came to exist decides which of the two you get, and that is a separate subject from this one. ## What this means for design The practical rule is to stop treating interception as something that applies to *a member* and start treating it as something that applies to *a call that crosses a boundary with a dispatch point on it*. Concretely: 1. Put the concerns you actually depend on at boundaries you control, where a call is made through a contract rather than to a concrete object. 2. Keep the members you expect to be wrapped replaceable and reachable, and treat a non-replaceable one carrying such a marker as a defect in the declaration. 3. Do not let anything important happen during construction if the concern is supposed to cover it. 4. Where a construct genuinely cannot be reached, make the concern explicit in the code rather than declaring it and quietly not getting it. ## What this is not It is not the self-call problem: these calls miss even when they come from outside through the correct reference. And it is not a question of which technique produced the stand-in; the list above is about what the language gives a stand-in to work with, not about how the stand-in was built.

  • Why is a member that belongs to the type rather than to an instance out of reach?
    Because the call names no receiver. The implementation is selected from the declared type before any object is involved, so there is no moment at which one object could be substituted for another. Covering it requires changing the member itself rather than standing in front of something.
  • A member that must carry the behaviour cannot be intercepted. What is the design response?
    Move the concern to a boundary that does have a dispatch point, typically a call the caller makes through a contract, or write the concern into the member's own code and accept the duplication. What you must not do is leave the declaration advertising behaviour the structure cannot deliver, since that gap produces no signal.

saying these in an interview costs you the question

  • Believes any member can be intercepted as long as the caller uses the stand-in.
  • Thinks reading a field goes through a member and can be wrapped.
  • Says inheriting a non-replaceable member's body is the same as intercepting it.
  • Assumes a wrapper added later still covers work done during construction.
  • Blames the caller's reference when the construct simply has no dispatch point.