A wrapper that forwards every call is still not the object it wraps: the inner object's own code, the callbacks it registers, and identity or type tests all see the inner one. Describe this split-identity problem (sometimes called self-schizophrenia) and how far different languages let you close the gap.
answer
- two selves: self-calls, escapes, identity, type tests
- NSProxy + respondsToSelector: best impersonation
- BasicObject proxy: almost everything hits method_missing
- Python `__class__` fools isinstance, `is` still tells
- Go: no hook - pass an interface instead
basics
~20 sWrapping creates two objects with one intended identity. The inner object's self-calls, the this it hands to callbacks, and identity or type checks all bypass the wrapper. Languages only narrow the gap: Objective-C NSProxy and Ruby BasicObject proxies impersonate well, Go offers nothing, and the durable fix is passing the outer identity in.
solid answer
~50 sForwarding produces two selves, so anything that escapes through the inner object escapes unwrapped. - **Objective-C** gets closest: an `NSProxy` implementing `forwardInvocation:` plus `respondsToSelector:` and `isKindOfClass:` can convincingly impersonate a target — at the price of a reflective dispatch path and blind static tooling. Pointer identity still differs. - **Ruby** proxies built on `BasicObject` inherit almost no methods, so nearly every message hits `method_missing`; `SimpleDelegator` even retargets at run time. `equal?` still separates them. - **Python** can fake type tests by defining a `__class__` property, so `isinstance` passes, while `is` and implicit dunder lookup still expose the wrapper. - **Go** has no interception hook whatsoever, so the language's own advice is to accept two identities and pass an interface. The mechanism-independent cure is to hand the inner object its outer identity explicitly, which is what real delegation does automatically.
code
python · 14 linesclass Proxy:
def __init__(self, target):
object.__setattr__(self, "_t", target)
def __getattr__(self, n):
return getattr(self._t, n)
@property
def __class__(self):
return type(self._t) # isinstance() now passes
t = Target()
p = Proxy(t)
isinstance(p, Target) # True - spoofed
p is t # False - cannot be spoofed
len(p) # fails unless __len__ is declared on Proxygo deeper
Know that a wrapper is a second object and that the wrapped object still refers to itself, so some calls skip your wrapper.
List the four leaks — self-calls, escaping references, identity, type tests — and give one language that can spoof type tests and one that cannot intercept at all.
Diagnose it in production terms: added behaviour missing on internally-triggered paths, duplicate registry entries, reflective branches taking the wrong arm; then apply wrap-at-the-boundary and no-self-escape.
Treat it as a contract decision for the wrapped type — declare the collaborator, forbid self-publication, and refuse designs that depend on a language-specific impersonation trick for correctness.
## What splits Wrap an object and you now have two runtime entities where the design intends one. Four things leak, and they leak in every language that only forwards: 1. **Self-calls.** A method inside the inner object calling another of its own methods never reaches the wrapper, so behaviour you added is skipped for internally-initiated work. 2. **Escaping references.** Whenever the inner object registers a callback, stores itself in a collection, returns `self` for chaining, or publishes an event carrying itself, the *unwrapped* object escapes. Everything downstream then talks to it directly. 3. **Identity.** Reference comparison distinguishes wrapper from wrapped, so caches, registries and identity-keyed maps see two entries. 4. **Type tests.** Runtime type checks, reflection and pattern matching report the wrapper's type, so code branching on concrete type takes the wrong branch. The classic name for the first item is *self-schizophrenia* (also 'broken delegation'), coined in the delegation-versus-forwarding literature; the other three are its practical consequences. ## How far each language lets you close the gap **Objective-C** is the high-water mark for impersonation. `NSProxy` is a root class with almost no inherited behaviour; you implement `methodSignatureForSelector:` and `forwardInvocation:` and every message routes through you. Because introspection is itself message-based, overriding `respondsToSelector:`, `isKindOfClass:` and `conformsToProtocol:` makes the proxy lie convincingly to reflective callers. Costs: a slow reflective dispatch path, static tooling that can no longer see what the object responds to, and pointer identity that still differs. **Ruby** takes the same route with `BasicObject`. A proxy subclassing it inherits only a handful of methods, so nearly every message falls into `method_missing`; you can also override `is_a?` and `class`. `SimpleDelegator` gives a ready-made version whose target is swappable via `__setobj__`. `equal?` still tells them apart, which is the point where the illusion ends. **Python** can spoof type tests: `__class__` is a settable/overridable property, and `isinstance` consults it, so a `__getattr__` proxy can pass `isinstance(p, Target)`. The `is` operator cannot be intercepted, and implicit special-method lookup goes to the real type, so `len()`, indexing and context-manager use expose the proxy unless declared. **Java and C#** confine interception to interfaces: `java.lang.reflect.Proxy` and C# `DispatchProxy` need an interface, so a concrete class's self-call is unreachable. Bytecode-weaving frameworks work around this by generating a *subclass*, which is why proxy-based frameworks on those platforms have the well-known rule that an internal self-call bypasses the proxy's added behaviour. **Go** deliberately provides nothing. Embedding promotes methods but the inner receiver is fixed and there is no `method_missing` equivalent, so a Go wrapper is always two objects. The idiomatic response is to stop pretending: the inner type takes a collaborator interface, and the wrapper supplies itself. **JavaScript** sits apart because its `Proxy` object intercepts every fundamental operation including property access and `has`, and because prototype-based delegation keeps one self by construction. Even so, `===` still distinguishes proxy from target, and code that stashed the raw object earlier keeps the raw object. ## The design responses that survive the language choice - **Wrap at the boundary, once.** If every reference to the inner object is created through a factory that wraps immediately, nothing unwrapped exists to escape. Split identity is mostly a problem of *late* wrapping. - **Forbid self-escape in the inner type.** A type intended to be wrapped should not return `self`, register `self`, or expose fluent chaining that leaks it; it should take the collaborator it needs as a constructor parameter. - **Pass the outer identity in.** Give the inner object an explicit `owner`/`context` reference and have it call that instead of itself. This is exactly what languages with true delegation do implicitly by re-binding self; writing it by hand makes the hook explicit and reviewable. - **Do not key on identity across a wrapping boundary.** Registries, caches and equality-by-reference are where the two-object reality surfaces first; key on a stable domain identifier instead. ## What a strong answer sounds like Say what leaks (four items), show you know the ceiling — that no forwarding mechanism in any language can make reference identity single — and then rank the languages by how much impersonation they permit, naming the price each charges. Finish on the design point: the only cure that works in every language is to let the inner object be told who its outer self is, which converts an accidental hook into a declared one.
- Which part of split identity can no forwarding mechanism in any language fix, and what follows from that?Reference identity. Even Objective-C's NSProxy and JavaScript's Proxy compare unequal to their targets under pointer or strict equality, because they really are distinct allocations. It follows that any design that keys on object identity across a wrapping boundary — identity maps, registries, reference-equality caches — will observe both objects. Key on a domain identifier instead, or make sure the raw object never exists outside the wrapper.
- How do proxy-based frameworks that generate a subclass instead of a wrapper change the picture, and what do they trade away?A generated subclass shares one object, so the added behaviour sits on the same instance rather than on a second one — whereas the common interface-proxy variety holds a separate target, which is why internal self-calls skip the added concern there. Subclass generation removes that gap at the cost of requiring an extensible type: non-final classes, non-private members, and a constructor the framework can call, which is a real constraint on the design of the wrapped type.
- What does 'pass the outer identity in' look like concretely, and why is it preferable to impersonation?The inner type declares the collaborator it needs — an interface, a callback, a context object — and calls that rather than itself; the wrapper passes itself when constructing the inner object. It is preferable because the extension point is declared, typed and reviewable, rather than being whichever self-calls the inner implementation happens to make today. It also works identically in Go, Rust, Java and Python, so the design does not depend on a reflective escape hatch.
saying these in an interview costs you the question
- Believing that forwarding every method makes the wrapper indistinguishable from the target.
- Overriding type tests to make a proxy pass isinstance and assuming identity comparison now agrees too.
- Blaming split identity on the wrapper author when the real cause is the inner object publishing references to itself.
- Assuming a proxy will intercept calls the object makes to its own methods.