skip to content

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%

answer

  1. Ruby singleton class sits ahead of the object's class, covers operators
  2. obj.class hides the singleton; dup and Marshal drop it
  3. JS own method shadows the prototype, costs hidden-class shape uniformity
  4. Python: instance function is unbound; dunders resolve on the type
  5. Self reinvented traits objects — classification returns for large populations

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.

solid answer

~60 s

- **Ruby** is the strongest: `def obj.render` creates a *singleton class* inserted ahead of the object's class, so per-object behaviour covers everything including operators. Costs: `obj.class` still reports the original class, so introspection understates the object, and `dup` and marshalling drop singleton methods. - **JavaScript** — an own function property shadows the prototype, so per-object methods are trivial. The cost is throughput: V8 assigns hidden classes per shape, and one-off shapes turn call sites polymorphic or megamorphic and defeat inline caches. - **Python** — the idiom mostly fails. Assigning a function to an instance attribute does not go through the descriptor protocol, so it is *not* bound and receives no `self`; and special methods such as `__len__` or `__add__` are looked up on the type, so per-object operator behaviour requires giving the object its own throwaway class (`obj.__class__ = type('X', (C,), {...})`). - **Self** built everything on exemplars and then reinvented shared 'traits objects' by convention — evidence that a large uniform population wants classification. - **Java, C#, C++, Rust** cannot express it: you subclass, or hold a function-valued field, which is why per-object variation there becomes configuration data.

code

ruby · 8 lines
ruby
config = Object.new
def config.+(other)
  "merged with #{other}"
end

puts(config + 'defaults')     # merged with defaults
puts config.class             # Object -- the singleton is hidden
puts config.singleton_class   # #<Class:#<Object:...>>

go deeper

for a junior

Know that some languages let one object have a method others of the same type do not, and name one such language.

for a middle

Explain Ruby's singleton class and JavaScript's own-property shadowing, and know Python instance functions are not bound.

for a senior

Add the failure modes — operator dispatch on the type in Python, serialization loss in Ruby, shape de-optimisation in JavaScript — and pick composition where the variation recurs.

for a principal

Decide the modelling stance for a system: exemplars for small hand-authored populations, classification for large uniform ones, and note Self's traits objects as the historical evidence that scale reinvents classes.

## Two ways to model variation Classification says: describe the kind, then stamp out members. Exemplar modelling says: describe *this* thing, and derive others from it if you need them. Prototype languages are the pure form of the second, but per-object behaviour is not exclusive to them — several class-based languages offer partial versions, and the differences between those partial versions are where the design judgment lives. ## Ruby: per-object behaviour done completely Writing `def config.reload` gives that one object a method. Ruby implements it by creating a hidden singleton class (also called an eigenclass) and splicing it into the lookup chain ahead of the object's class. Because it is a real class in the chain, everything works there: normal methods, operators such as `+` and `[]`, `attr_accessor`, and module inclusion via `extend`. Ruby's class methods are simply singleton methods on the class object, which is why the model is so uniform. The costs are about honesty and lifetime. `obj.class` reports the original class, not the singleton, so a reader inspecting the type learns less than the object can do; `obj.singleton_class` is required to see the truth. `dup` does not carry singleton methods over (`clone` does), and marshalling refuses objects with singleton methods, which quietly rules the idiom out for anything that must be serialized or moved across a process boundary. ## JavaScript: easy, and the runtime charges for it An own property shadows the prototype, so `obj.render = function () {...}` gives one object its own behaviour. It is idiomatic in event handling and configuration objects. The cost is performance rather than correctness: engines group objects by shape (hidden class) and specialise call sites by the shapes they have seen. Objects that acquire ad-hoc members each get their own shape, so a call site iterating a heterogeneous population goes polymorphic and then megamorphic, losing inline caching. For a handful of authored objects that is irrelevant; across a hot loop over thousands of entities it is measurable, and it is the reason performance-sensitive JavaScript keeps object shapes uniform. ## Python: the idiom that looks available but is not Python appears to allow it — `obj.render = lambda: ...` assigns fine — but two mechanisms defeat it. First, binding: functions become methods through the descriptor protocol, which runs only for attributes found on the *type*. An instance-dict function is returned as-is, so it receives no `self` and any implementation needing state must close over the object explicitly. Second, and more decisively, special methods are looked up on the type. `len(obj)` consults `type(obj).__len__`; an instance-level `__len__` is ignored entirely. The workaround Python programmers reach for is to give the object its own class: `obj.__class__ = type('OneOff', (C,), {'__len__': ...})`. That is an exemplar expressed by manufacturing a class per object — cheap in Python, but it makes the point that the class remains the only holder of dispatchable behaviour. ## Self, and the return of classification Self removed classes entirely: objects have slots, some pointing at parent objects, and you build new objects by cloning. In practice the community converged on 'traits objects' — parent objects holding shared behaviour, with per-object slots for state. That convention is a class in all but name, and it is the strongest available evidence for when classification wins: once a population is large and uniform, you want one place to change behaviour, one thing for tooling to index, and one description to review. ## The static languages Java, C#, C++ and Rust cannot attach behaviour to an instance at all. The equivalents are subclassing (heavy for one object), a field holding a function or strategy object (which turns behaviour into data), or code generation. This is why per-object variation in those ecosystems surfaces as configuration, rules tables and dependency injection rather than as ad-hoc methods. That is a genuine benefit as well as a limit: the set of behaviours is enumerable, so tooling, review and static analysis all see the whole space. ## How to decide Exemplars win where the population is small, authored by hand, and genuinely individual — a level's boss enemy, a specially-behaved widget, a test double that needs one method to lie. Classification wins where the same variation recurs, where many people must find and change it, where serialization or remoting crosses process boundaries, or where the hot path benefits from uniform shapes. The middle path most ecosystems settle on is composition: keep one type and give it a behaviour-valued field, which is expressible in every language named here and keeps the variation visible in the type.

  • A Ruby object gains a singleton method and is later sent to another process. What happens, and what does that imply for the idiom?
    Marshal refuses to serialize an object with singleton methods, and dup does not carry them (clone does). So the behaviour exists only inside the process that attached it. The implication is that exemplar modelling is for objects that live and die locally; anything crossing a boundary must have its variation expressed as data plus a shared type.
  • When does composition beat both exemplars and subclassing for per-object variation?
    When the variation recurs and must be visible in the type: give one class a field holding a strategy or callback and select it per instance. It is expressible in every language including Java, C# and Rust, it keeps object shapes uniform so JavaScript engines stay fast, and it survives serialization because the choice is data. Exemplars remain better only for genuinely one-off, locally authored objects.

saying these in an interview costs you the question

  • Assuming Python instance attributes behave like Ruby singleton methods — they are unbound and never affect operator dispatch.
  • Claiming per-object methods are free in JavaScript, ignoring hidden-class and inline-cache effects on hot paths.
  • Believing a Ruby object's class reflects its singleton methods, or that dup and Marshal preserve them.
  • Treating prototype languages as inherently classless in practice; Self converged on traits objects and JavaScript converged on class.
  • Recommending exemplars for a large uniform population where one shared definition and tooling support matter more.

context