skip to content

Delegation & Forwarding

Implementing behavior by handing requests to a held object — plain forwarding versus true delegation where 'this' still means the outer receiver. Interviewers probe it beneath decorators and proxies.

on this pageshow

questions

3

Several languages ship built-in help for making one object stand in for another: Kotlin's `by` clause, Scala 3's `export`, Go struct embedding, Ruby's Forwardable/SimpleDelegator, and Python's `__getattr__`. Compare what each of them actually generates, and what each one cannot do.

level: middleimportance: should knowfreq 38%

answer

  1. all five generate forwarding, never delegation
  2. Kotlin `by`: interface required, fixed at construction
  3. Scala 3 `export`: forwarders without subtyping
  4. Go embedding: name promotion, no interception hook
  5. `__getattr__` skipped by len()/[] — dunders on the type

basics

~20 s

All of them generate forwarding, not delegation. Kotlin by, Scala 3 export and Go embedding emit compile-time stubs that call the inner object; Ruby's Forwardable metaprograms named methods; Python's __getattr__ forwards at run time but is skipped by implicit special-method lookup.

solid answer

~50 s

They differ on three axes: whitelist vs whole surface, when the target is bound, and whether there is an interception seam. - **Kotlin** `class L(d: Repo) : Repo by d` emits one stub per interface member; the delegate is stored at construction and can never be swapped, and the interface is mandatory. - **Scala 3** `export inner.*` generates the same forwarders but as ordinary members, with rename/exclude, and it does **not** make you a subtype — delegation and subtyping are separate declarations, unlike Kotlin's `by`. - **Go** embedding promotes names at compile time and carries interface satisfaction along, but offers no hook to intercept a promoted call. - **Ruby** `def_delegators` whitelists methods at class-definition time; `SimpleDelegator` forwards via `method_missing` and can retarget at run time with `__setobj__`. - **Python** `__getattr__` forwards everything, including typos, but `len(w)` and `w[i]` look the dunder up on the type and never reach it.

code

python · 10 lines
python
class Wrap:
    def __init__(self, inner):
        self._inner = inner
    def __getattr__(self, name):
        return getattr(self._inner, name)

w = Wrap([1, 2, 3])
w.append(4)      # forwarded, works
w.__len__()      # 4  - explicit call is forwarded
len(w)           # TypeError: object of type 'Wrap' has no len()

go deeper

for a junior

Know that these features write the boring pass-through methods for you, and that they call the inner object rather than making it aware of you.

for a middle

Be able to place each mechanism on the whitelist-vs-whole-surface and compile-time-vs-run-time axes, and to name the Python special-method-lookup trap.

for a senior

Choose deliberately: pick the dynamic mechanisms when you need one interception seam or a swappable target, the static ones when you want the compiler to keep the surface in sync.

for a principal

Reason about the cost across a codebase: catch-all forwarding hides surface drift from review and tooling, static generation makes every upstream member addition a visible recompile.

## The problem these features solve When one object stands in for another — a wrapper that adds logging, a decorator that adds caching, an adapter that renames — you have to re-expose the inner object's methods. Written by hand that is one stub per member: `fun m(a) = inner.m(a)`. For a twenty-member interface that is twenty bodies with no logic in them, which must be revisited every time the interface grows. Several languages ship a feature that writes those stubs for you. The single most important fact about all of them: they generate **forwarding**. Each produces (or simulates at run time) a call `inner.m(...)`. None of them tells the inner object that it is being wrapped, and none re-binds the inner object's notion of self. ## Kotlin: the `by` clause `class Logged(private val d: Repo) : Repo by d` makes the compiler emit an implementation of every member of `Repo` that calls `d.m(...)`. Constraints worth naming: the delegated type must be an **interface**; the delegate expression is evaluated once at construction and stored in a hidden field, so there is no run-time swap; and any member you override in `Logged` is invisible to `d` — code inside `d` calling `m()` runs `d`'s own `m`. ## Scala 3: `export` `export inner.*` generates forwarders as ordinary members of the enclosing class or object, and you can select (`export inner.{read, write}`), rename, or exclude members. Nothing about `export` makes your class a subtype of the exported type. That is the sharpest contrast with Kotlin: in Kotlin, `by` is simultaneously the subtyping declaration and the forwarding declaration, so you get the whole interface or nothing; in Scala 3 you decide those two things separately. ## Go: struct embedding `type Wrap struct { Inner }` promotes `Inner`'s methods and fields onto `Wrap` by name at compile time. Promotion is shallowest-wins: a method declared directly on `Wrap` shadows the promoted one, and two embedded types offering the same name at the same depth are not an error until a call site selects it. Promotion also carries interface satisfaction — `Wrap` satisfies whatever `Inner` satisfies, which is why embedding is Go's mixin idiom. The receiver of a promoted method is still the `Inner` value, and Go supplies no interception hook at all. ## Ruby: Forwardable and SimpleDelegator `extend Forwardable; def_delegators :@inner, :read, :write` metaprograms named methods when the class body runs — an explicit whitelist, compiled once. `SimpleDelegator` is the catch-all sibling: it forwards unknown messages through `method_missing`, and it lets you replace the target at run time with `__setobj__`. That retargeting is something none of the compile-time features can do. ## Python: `__getattr__` `def __getattr__(self, name): return getattr(self._inner, name)` is the most permissive mechanism on the list. It forwards everything the wrapper does not define, including names that are typos, and it works on objects whose surface is not known statically. It also has a trap that catches people repeatedly: **implicit special-method invocation looks the dunder up on the type, not the instance**, so `len(w)`, `w[i]`, `with w`, `w + y` and iteration never reach `__getattr__`. `w.__len__()` works; `len(w)` raises `TypeError`. Wrapping anything protocol-shaped in Python means writing the dunders out explicitly. ## The three axes to remember 1. **Whitelist vs whole surface.** Scala's `export` and Ruby's `def_delegators` name members. Kotlin's `by` and Go embedding take a declared type's entire surface. `__getattr__` and `method_missing` take everything, known or not. 2. **When the target is bound.** Compile time for Kotlin, Scala and Go; class-definition time for `def_delegators`; per call for `__getattr__` and `SimpleDelegator` — and only that last group can swap the target on a live object. 3. **Whether there is a seam.** Dynamic mechanisms give you exactly one place to add a cross-cutting concern (timing, retries, auditing) for every member. The static generators give you none: to instrument one member you must write it by hand and let the generator cover the rest. ## What none of them buy you No mechanism here makes the inner object cooperate with the outer one. The inner object's self-calls, the identity it hands to callbacks, and the type it reports all remain the inner object's. That is the same for Kotlin, Scala, Go, Ruby's Forwardable and Python's `__getattr__` — which is why the language-level support removes typing, not the design consequences of wrapping.

  • Which of these mechanisms can change the object being forwarded to after construction, and why can the others not?
    Only the run-time ones: Ruby's SimpleDelegator via `__setobj__`, and any Python `__getattr__` wrapper that reads a mutable attribute. Kotlin's `by` stores the delegate in a hidden field created at construction with no setter, Scala's `export` bakes the forwarders against a specific path, and Go's promotion is a compile-time name resolution. If you need swapping in those languages you must write the field and the stubs yourself.
  • Why does wrapping a Python object that supports `len()`, indexing or `with` require more work than wrapping a plain service object?
    Because CPython looks special methods up on the type object for implicit invocations, bypassing both `__getattr__` and instance attributes. A `__getattr__`-based wrapper therefore forwards `read()` fine but fails `len(w)`, `w[0]` and `with w`. You must declare `__len__`, `__getitem__`, `__enter__`/`__exit__` and friends explicitly on the wrapper, which is why library wrappers there are much longer than the one-line `__getattr__` suggests.

saying these in an interview costs you the question

  • Calling Kotlin's `by` or Go embedding 'delegation' in the prototype-language sense — they never re-bind the callee's self.
  • Assuming a Python `__getattr__` wrapper is a drop-in replacement, then being surprised that `len()` or `with` fails on it.
  • Believing Go embedding lets the outer type override the inner type's behaviour for the inner type's own calls.
  • Thinking Scala 3's `export` makes the enclosing class a subtype of the exported type.

context

open as a page

Distinguish forwarding — an object holding another and calling the same method on it — from true delegation as the term is used in prototype-based languages. Name languages that actually provide the latter, and explain what the difference changes for the wrapped object's own self-calls.

level: seniorimportance: should knowfreq 40%

basics

~20 s

Forwarding hands the call to another object whose self is itself. True delegation re-binds self to the original receiver, so the delegate's self-calls land back on the outer object. JavaScript prototypes, Self and Ruby's included modules delegate; Kotlin's by and Go embedding only forward.

open as a page

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.

level: principalimportance: nice to knowfreq 26%

basics

~20 s

Wrapping 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.

open as a page