skip to content

Objects, State & Behavior

An object bundles state with the behavior that governs it, answers the messages it is sent, and has an identity distinct from its value. Interviewers open here because everything later assumes it.

on this pageshow

questions

14

Some languages treat a call written as receiver.doThing() as a message the receiver resolves at run time; others compile it into a fixed member call. Contrast the two models using specific languages, and say what each buys and costs.

level: middleimportance: should knowfreq 42%

answer

  1. selector is data vs vtable slot index
  2. doesNotUnderstand: / method_missing / __getattr__
  3. respond_to_missing? or introspection lies
  4. Go has no hook: mocks are codegen
  5. Python dunders resolve on the type, not the instance

basics

~20 s

Under messaging (Smalltalk, Objective-C, Ruby, Python) the method name is a runtime value the receiver resolves, so an unknown name reaches a hook: doesNotUnderstand:, method_missing, getattr. Under member calls (C++, Go, Rust) the target is fixed at compile time and an unknown name does not compile.

solid answer

~1 min

Two different questions are being answered: who decides what this call means, and when. - **Smalltalk / Objective-C** — the selector is a runtime value. If the receiver has no method for it, control reaches `doesNotUnderstand:` / `forwardInvocation:`, which is how `NSProxy` and distributed-object stubs exist at all. Price: a cached dictionary lookup per send, and the runtime crash `unrecognized selector sent to instance` for a mistake other languages catch while compiling. - **Ruby / Python** — the same hook renamed: `method_missing` (plus `respond_to_missing?`, which you must also write or introspection lies about the object) and `__getattr__`, which fires only after normal lookup fails. Python deliberately narrows the power: operator dispatch is looked up on the type, so `__getattr__` cannot fabricate `__len__`. - **C++ / Rust / Go** — the name is erased. A C++ virtual call is a vtable slot index, so there is nothing to intercept; Go checks method sets against interfaces at compile time and has no missing-method hook, which is why Go mocks are code generation rather than runtime magic. You buy devirtualization, inlining and completeness in tooling. - **JavaScript** is the middle case: calls are property lookups, interceptable only through a `Proxy` created around the object up front — you cannot retrofit interception onto a reference someone already holds. So: messaging when the receiver set is open (ORMs, RPC stubs, DSLs); member calls when you want the compiler and the IDE to know the answer.

code

ruby · 11 lines
ruby
class Row
  def method_missing(name, *args)
    return @cols[name] if @cols.key?(name)
    super
  end
  def respond_to_missing?(name, priv = false)
    @cols.key?(name) || super
  end
end

row.email   # works; no such method was ever defined

go deeper

for a junior

Be able to say that in some languages the method name still exists while the program runs and in others it does not, and give one example of each.

for a middle

Name the actual hooks (doesNotUnderstand:, method_missing, getattr) and the fact that Go and C++ have none, plus one consequence such as mocking style.

for a senior

Discuss the price of each model: runtime crashes and introspection drift versus code generation and the inability to proxy, and pick a side for a given open-or-closed receiver set.

for a principal

Frame it as where verification lives. Choosing messaging pushes contract checking into tests and runtime observability; choosing member calls pushes flexibility into build-time generation. Argue the choice from how open the system's object population is.

## The claim under the syntax Every object language writes calls the same way, so the syntax hides a real fork. In one family, `receiver.name(args)` means *send the symbol `name` to this object and let it decide*. In the other it means *jump to the entry the compiler already picked for this static type*. Everything else in this question follows from that fork. ## Messaging: the name survives to run time Smalltalk's model is that an object is a black box that answers messages. The selector (`printOn:`, `at:put:`) is a first-class value; sending is a lookup in the receiver's method dictionary, walking up its superclass chain, with an inline cache to make it affordable. If the walk fails, the runtime does not error — it re-sends `doesNotUnderstand:` with the original message reified as an object. That single hook is the whole basis of proxies. Objective-C inherits this directly: `objc_msgSend` takes a `SEL`, and unhandled selectors get a forwarding pipeline (`forwardingTargetForSelector:`, then `forwardInvocation:`). `NSProxy` is a root class with almost no methods precisely so that everything falls into forwarding. Ruby's `method_missing` and Python's `__getattr__` are the same idea in dynamic-language clothing: ActiveRecord's `find_by_email` and countless RPC stubs are methods that never existed until called. The cost has three parts. First, performance: a send is a lookup plus caching, not a call. Second, honesty of introspection: in Ruby, if you implement `method_missing` without `respond_to_missing?`, then `respond_to?` says the object cannot do something it demonstrably does, and serializers, documentation tools and completion all inherit the lie. Third, the failure moves to run time — Objective-C's `unrecognized selector sent to instance` is a production crash for a typo. ## Member calls: the name is erased In C++ a non-virtual call is a direct call and a virtual call is an indexed load from a vtable. After compilation the string `doThing` is not present in any dispatch path. There is therefore no interception point: a proxy in C++ is a class that mechanically re-declares every member of the interface and forwards it, which is why templates and code generation carry that weight. Go goes further: interface satisfaction is structural but checked when compiling, and there is no fallback for an absent method. Rust resolves methods against traits in scope, and even the ability to add a method to a foreign type is constrained by the orphan rule. What these languages buy is that the compiler and the tooling always know the full answer, and the optimizer can devirtualize and inline. ## The middle ground JavaScript looks dynamic but is not a messaging language: a call is a property read followed by an invoke. Interception exists only through `Proxy`, and a `Proxy` must wrap the object before anyone gets a reference to it — there is no `doesNotUnderstand:` on a plain object. Python is deliberately half-open in a different way: `__getattr__` catches ordinary attribute misses, but special (dunder) methods are looked up on the type, so a proxy that forwards `__len__` or `__add__` must define them explicitly. ## Why interviewers care Because it explains why a mocking library looks like a one-liner in Ruby and a code generator in Go, and why an ORM can invent query methods in one ecosystem and requires a build step in another. It also frames a real design decision: a system with an open receiver set (plugins, remote objects, schemas discovered at run time) fights the compiler in the member-call family, while a system that needs static verification and IDE navigation fights the runtime in the messaging family. ## Responsibilities restated The messaging view has a modelling consequence worth saying out loud: an object's responsibilities are the set of messages it answers, not the fields it holds. In a messaging language that set can genuinely be computed at run time; in a member-call language the set is fixed when compiling and therefore *is* the declared interface. Both are the same idea, but only one of them can change after the program starts.

  • A Ruby object answers calls through method_missing but respond_to? returns false for them. What breaks, and what is the fix?
    Anything that asks before calling: duck-typing checks, serializers, form builders, delegation helpers and REPL completion all conclude the object cannot do it. The fix is to implement respond_to_missing? (and often __dir__ or methods) alongside method_missing so introspection and behaviour agree. Objective-C has the same obligation via respondsToSelector:.
  • How do C++ or Go get proxy-like behaviour without a missing-method hook?
    They move the work to compile time. C++ uses templates or generated forwarding classes that re-declare each member; Go generates mock types from an interface, or embeds an interface value in a struct so the struct satisfies the interface while unimplemented methods panic if actually called. Both approaches give you the compiler's guarantee back in exchange for a build step.

saying these in an interview costs you the question

  • Saying every object language looks a method up by name at run time — C++, Rust and Go erase the name entirely.
  • Claiming method_missing or __getattr__ is free; it costs lookup, tooling accuracy and static verification.
  • Assuming a Python __getattr__ or a JS Proxy will intercept operator/dunder dispatch on the type.
  • Thinking an unknown message quietly returns nil rather than reaching a hook or raising.
  • Calling JavaScript a message-passing language in the Smalltalk sense — it has property lookup plus Proxy, not a doesNotUnderstand hook.

context

open as a page

In a delegation-based (prototype) object model, describe what happens when you read a name the object does not have, and what happens when you assign to that same name. Contrast that with lookup in a class-based language, naming specific languages.

level: middleimportance: should knowfreq 46%

basics

~20 s

Reads walk the delegation chain until a holder is found; writes normally do not walk — they create an own property on the receiver that shadows the parent's. So prototype state looks shared until the first assignment, while mutating a prototype's array or object stays shared for everyone.

open as a page

Some languages give object creation dedicated syntax that always yields a fresh instance of exactly the named type; others build objects with ordinary functions. Compare the two using Go, Rust, Python and Java, and say what the dedicated syntax makes impossible.

level: middleimportance: should knowfreq 45%

basics

~20 s

Java's and C++'s new T is pinned to T: it cannot return a cached instance, a subtype, or a failure value, so anything interesting moves into a separate function. Go, Rust and Python's new have no such rule -- construction there is a normal function returning a value, so those abilities need no pattern name.

open as a page

Some languages make the class itself an object that can be stored in a variable, passed as an argument and sent messages; others treat class-level (static) members as namespaced functions with no receiver at all. Compare what a first-class class object buys you, and what the receiver-less model forces you to build instead.

level: middleimportance: should knowfreq 42%

basics

~20 s

Where the class is itself an object — Smalltalk, Ruby, Objective-C, Python's classmethod — class-level code has a receiver, so it is inherited, dispatched, and passable as a value. Java, C++ and C# statics have no receiver, so you inject factory objects or use reflection.

open as a page

In some languages the receiver of a method is written as an explicit parameter (Python's self, Go's receiver declaration, Rust's &self) while in others it is an implicit this. What can a language express by making the receiver explicit, and what does it pay for that?

level: seniorimportance: should knowfreq 34%

basics

~20 s

An explicit receiver is a parameter, so its type and mode can vary. Go distinguishes value from pointer receivers, which decides interface satisfaction; Rust encodes ownership (&self, &mut self, self consumes the object); Python makes methods plain functions bound through the descriptor protocol.

open as a page

A runtime type test -- Python's isinstance, a Go type assertion on an interface, JavaScript's instanceof -- appears to ask the same question of an object. What is each one actually checking, and where do they give different answers for objects of the same shape?

level: seniorimportance: should knowfreq 42%

basics

~20 s

They ask three different questions. Python's isinstance asks declared-or-registered membership, and a metaclass hook can make it say yes without inheritance. Go asks whether the concrete type's method set covers the interface, with no declaration anywhere. JavaScript's instanceof asks whether one specific constructor's prototype object is on the chain, so it fails across realms.

open as a page

JavaScript's class keyword is usually described as syntax sugar over prototypes. Which parts really are sugar, which parts cannot be written with the older prototype pattern, and how does that compare with class declarations in other dynamic languages?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Prototype wiring and method placement are sugar. Not sugar: derived construction, where the base allocates the instance via new.target so extending Array, Error or Map works; #private fields, which are not properties at all; the this-before-super dead zone; constructors that refuse to be called without new; non-enumerable methods; strict-mode bodies.

open as a page

When a base type's initialization code calls a method that a derived type overrides, the language must decide what the object's type already is at that moment. Explain the decision, and how C++, Kotlin, Swift and Rust each resolve it and what each resolution costs.

level: seniorimportance: should knowfreq 48%

basics

~30 s

An object under construction has no stable type yet. C++ changes it as construction proceeds, so a base constructor sees the base's own version and a pure virtual call aborts. Kotlin, Java and C# treat the object as its final class immediately, so an override runs before its own state exists -- Kotlin can even observe null in a non-null val. Swift forbids the observation at compile time; Rust cannot express it.

open as a page

Mutable class-level state is a global variable wearing a class name. Compare how different languages' initialization and isolation models change how dangerous that is, and what each of them actually gives you for testing code that depends on it.

level: seniorimportance: should knowfreq 48%

basics

~20 s

Class-level mutable state has process-wide lifetime and no owner. Languages diverge sharply: Rust forces a OnceLock or Mutex, C# gives each closed generic type its own copy while Java's erasure gives one, Python lets a test rebind the attribute, Go offers no reset hook, Elixir has no such state at all.

open as a page

Construction is meant to yield either a valid object or no object at all. When it fails partway, what becomes of the memory, the already-initialized parts, and any reference that already escaped? Contrast how C++, Python, Java and Rust handle it.

level: principalimportance: should knowfreq 30%

basics

~20 s

The contract holds to different depths. C++ destroys completed bases and members but never runs the failed object's own destructor. Python already allocated the object in new, so a failing init still triggers del on a half-built instance. Java loses the reference but not anything the constructor registered. Rust makes the half-built value unrepresentable.

open as a page

Some runtimes let a running program manufacture a brand-new class: Python's built-in `type(name, bases, namespace)`, Ruby's `Class.new`, and the Objective-C runtime's `objc_allocateClassPair` + `objc_registerClassPair` pair. Several also fire a hook the instant a subtype appears — Python's `__init_subclass__`, Ruby's `inherited`. Other platforms fix the set of types before the program starts: Rust has no runtime class construction, Go's `reflect.StructOf` can fabricate a struct shape but cannot attach methods to it, and a GraalVM native-image binary only contains types the build could see. What does runtime type construction buy a framework author, what does closing the type set buy in return, and how do you decide which cost your system pays?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Runtimes that can build a class while running — Python's type(), Ruby's Class.new, Objective-C class pairs — let frameworks derive types from data and let subtypes self-register. Closing the type set instead buys ahead-of-time layout, dead-code stripping, and types you can actually see.

open as a page

Some languages let a single object carry behaviour that nothing else has, without declaring a new type for it. Where is that idiom genuinely better than defining a class, and where does it break down? Use specific languages.

level: principalimportance: nice to knowfreq 25%

basics

~20 s

Exemplar modelling suits small, hand-authored populations where instances differ individually: game entities, UI widgets, test doubles. It breaks on tooling and static reasoning, and each language limits it differently — Ruby's singleton class covers even operators, Python's instance attributes do not bind self and never affect operator dispatch, JavaScript pays in shape de-optimisation.

open as a page

Generic code often needs an operation that belongs to a type rather than to any instance — 'produce the identity value', 'parse one of these from text', 'give me an empty builder'. Some languages let an interface or constraint require such a member; others cannot express it at all. Compare the designs and the price each one pays.

level: principalimportance: nice to knowfreq 28%

basics

~20 s

Requiring a type-level operation means dispatching without a receiver. Haskell picks the implementation from the expected result type via dictionaries; Rust and Swift allow such requirements but the trait or protocol stops working as dyn/existential; C# 11 adds static abstract members usable only through a type parameter; Java must hand in a factory object.

open as a page