A wrapped member is also called by the object on itself, so which restructurings make that internal call go through the stand-in?
answer
- the call must leave the object
- change the receiver, not the declaration
- extract onto a collaborator
- or hold a reference to your own stand-in
- reference must point at the stand-in
basics
~20 sMake the internal call cross an object boundary. Either move the member onto a separate collaborator the target calls through a reference, or give the target a reference to its own stand-in and have it call through that.
solid answer
~40 sTwo structural routes and one ambient one. **Extraction**: move the member onto a collaborator the target holds a reference to, wrap that collaborator, and the internal call becomes a call to another object, so the behaviour fires. This is the honest fix, because it makes the seam visible in the design. **Self-reference**: hand the target a reference to its own stand-in and have it call through that; cheap, but it creates a wiring cycle and the object now knows it is wrapped. **Ambient lookup**: fetch the current stand-in from shared context at call time, which works but hides the dependency and raises scoping questions. Whatever the route, the invariant is identical: the call must travel through a reference that points at the stand-in.
code
pseudocode · 14 lines// before: the inner call stays inside the object
object Orders:
method placeAll(list):
for each order in list: price(order) // receiver is the object
method price(order): ...
// after: the inner call crosses a boundary
object Orders(pricing):
method placeAll(list):
for each order in list: pricing.price(order)
object Pricing:
method price(order): ...
wired = Orders(CachingStandIn(Pricing())) // now price() is interceptedgo deeper
Recall that the fix is to make the internal call go to a different object, not to change how the member is declared.
Explain each route and say why it works in one sentence: after the change, the call travels through a reference that points at the stand-in.
Weigh the routes against the codebase in front of you: which split is honest, what the construction cycle costs, and how you will demonstrate the behaviour now applies.
Decide whether a concern should depend on a mechanism that needs this workaround at all, and what design rule keeps the next team from recreating the gap.
## The invariant every real fix satisfies Added behaviour runs where a call arrives at a stand-in. So the only way to cover a call the object makes on itself is to change **which object receives it**. Every workable fix is a rearrangement that makes the receiver be the stand-in instead of the target; every non-fix is an attempt to change something else and hope. That gives you a single test to apply to any proposal: *after the change, does the call travel through a reference that points at the stand-in?* If not, the proposal does nothing, however plausible it reads. ## Route 1 - extract the member onto a collaborator Move the member out of the target and into a separate object. The target holds a reference to that collaborator, the wiring hands it the wrapped collaborator, and what used to be an internal call is now a call from one object to another, crossing exactly the boundary interception needs. This is usually the right answer, and not only because it works. An internal call to a member that someone wanted to wrap is often a responsibility the object was quietly doing on its own behalf. Extraction names that responsibility and gives it a seam. The costs are real: a new type, a new wiring edge, and occasionally an awkward split where the extracted member needs state that stayed behind. ## Route 2 - let the target hold its own stand-in Give the target a reference to the very stand-in that wraps it, and have the internal call go through that reference. The receiver is now the stand-in, so the behaviour fires. The costs concentrate in two places. First, **wiring**: the stand-in wraps the target, and the target references the stand-in, so construction has a cycle to resolve, which is fragile and easy to get wrong when the object graph is assembled automatically. Second, **coupling**: the target now knows it is wrapped. The premise of the technique was that the target did not have to care, and this route spends that premise to buy the fix. ## Route 3 - look up the current stand-in from ambient context Some systems publish the stand-in for the call in progress in a shared, call-scoped place; the target fetches it and calls through it. It works, and it avoids the construction cycle. It also hides the dependency: nothing in the target's signature says it needs an ambient value, so the member fails or silently reverts when invoked outside a context that published one. The scoping rules (per call, per thread of execution, per task) become something every caller has to respect. | route | receiver after the change | what it costs | |---|---|---| | extract to a collaborator | the collaborator's stand-in | a new type and a new wiring edge | | hold your own stand-in | the stand-in itself | a construction cycle, and the target knows it is wrapped | | ambient lookup | the published stand-in | a hidden dependency and scoping rules to respect | | leave it, apply the concern in the member's code | not applicable | duplication, but no silent gap | A fourth option abandons wrapping altogether and changes the member itself rather than standing in front of it; that is a different mechanism with different build and tooling costs and is not this subject. ## Fixes that look like fixes and are not - **Re-declaring the marker that requests the behaviour on the inner member.** The marker is read by whoever builds the stand-in; it has no effect on a call that does not reach one. - **Qualifying the internal call explicitly with the self-reference.** Writing the receiver out makes the code clearer and changes nothing: the self-reference still points at the target. - **Widening the inner member's visibility or making it overridable.** That may be necessary for some stand-ins to exist at all, but it does not put one on this call path. - **Adding a test that calls the inner member from outside.** That test exercises the path that already worked and will pass forever while the bug ships. ## Proving the fix Assert on the **effect** of the added behaviour, from the path that used to bypass it: a second call served from the cache, an audit record, an incremented retry count. Drive the outer member, not the inner one. If the behaviour leaves no observable trace at all, the first fix is to give it one, because a stand-in that ran and a stand-in that did not run must be distinguishable before any of this can be verified.
- Which structural route would you prefer by default, and why?Extraction. The internal call was usually hiding a responsibility worth naming, and moving it out gives that responsibility a seam while keeping the target unaware that anything wraps it. The self-reference route is quicker but couples the object to the fact of being wrapped, and it survives less well when the wiring changes.
- The member cannot be extracted because it needs state that must stay on the object. Now what?Either move the state with it, or stop routing this concern through a stand-in and put it in the member's own code, accepting the duplication. The one option to reject is leaving the call path uncovered while the declaration still advertises the behaviour, because that is the gap nobody notices.
saying these in an interview costs you the question
- Says re-declaring the marker on the inner member fixes it.
- Thinks writing the self-reference out explicitly changes the receiver.
- Treats the self-reference route as free and ignores the wiring cycle.
- Copies the added behaviour by hand into the target and expects it to stay in sync.
- Declares the fix done without exercising the path that used to bypass it.