skip to content

In Ruby, what is the superclass chain above an ordinary class like Vehicle, and when would you subclass BasicObject instead of Object?

level: middleimportance: should knowfreq 36%

answer

  1. Object is the default parent
  2. one more class above Object
  3. the root's superclass is nil
  4. Kernel is a module, not a class
  5. blank slate for proxies

basics

~20 s

Vehicle.superclass is Object, Object.superclass is BasicObject, and BasicObject.superclass is nil. Kernel is a module Object includes, not a class. Subclass BasicObject for a near-empty proxy or DSL object where method_missing should catch almost every call.

solid answer

~40 s

A class declared without `<` inherits from `Object`; `Object.superclass` is `BasicObject`, and `BasicObject.superclass` is `nil`, since it is the only class with no parent. Most everyday methods (`puts`, `inspect`, `class`, `respond_to?`, `is_a?`, `raise`) come from the `Kernel` module that `Object` includes, and `Class#superclass` skips modules, so `Kernel` never appears in a `superclass` walk. `BasicObject` keeps only a handful of public methods: `==`, `equal?`, `!`, `!=`, `__id__`, `__send__`, `instance_eval` and `instance_exec`, plus private hooks such as `method_missing`. That makes it a **blank slate**: the standard library's `Delegator` inherits from it so almost every call falls through to `method_missing`. The cost is that top-level constants need `::String` and helpers need `::Kernel.puts`.

code

ruby · 16 lines
ruby
class Recorder < BasicObject
  def initialize
    @calls = []
  end

  def method_missing(name, *args)
    @calls << name
    self
  end

  def calls = @calls
end

r = Recorder.new
r.to_s.class.inspect
r.calls  # => [:to_s, :class, :inspect]

go deeper

for a junior

Recall the chain Vehicle, Object, BasicObject and that BasicObject.superclass is nil; know that everyday helpers like puts come from Kernel.

for a middle

Explain that Kernel is a module included by Object, which is why superclass skips it and why a BasicObject subclass lacks puts, inspect and class.

for a senior

Justify BasicObject for proxies and DSL builders, as Delegator does, and list the fixes it forces: ::Kernel.raise, ::String and a hand-written inspect.

for a principal

Weigh a blank-slate proxy against a plain Object subclass with explicit delegation: fewer name collisions against weaker debuggability and surprising behaviour in tooling.

## The chain from your class to the root Every Ruby class except one has exactly one superclass, and `Class#superclass` returns it: ```ruby class Vehicle; end Vehicle.superclass # => Object Object.superclass # => BasicObject BasicObject.superclass # => nil ``` - **`Object`** is the default parent: any class written without `<` inherits from it. - **`BasicObject`** is the parent of `Object`, and the root of the class tree. - `BasicObject.superclass` returns **`nil`**: it is the only class with no parent. You can print the chain yourself with a loop that stops at `nil`: ```ruby klass = Vehicle while klass puts klass klass = klass.superclass end # Vehicle # Object # BasicObject ``` The loop ends because `BasicObject.superclass` is `nil`, which is falsy. Note what it does not print: `Kernel`. ## Where Kernel fits Methods such as `puts`, `p`, `raise`, `inspect`, `class`, `respond_to?`, `is_a?` and `frozen?` are not defined on `Object` itself. They live in **`Kernel`**, a **module** that `Object` includes, which is why they are available on every ordinary object and, as private methods like `puts`, callable without a receiver. Because `Kernel` is a module and not a class, `Class#superclass` skips it: walking `superclass` from `Vehicle` never lands on `Kernel`. (The full lookup order, which does list included modules, is a separate topic.) The split has a purpose: `BasicObject` sits below both `Object` and `Kernel`, so a class that inherits straight from `BasicObject` gets neither. ## What BasicObject keeps | Kept on BasicObject | Missing (they come from Kernel via Object) | |---|---| | `==`, `!=`, `!`, `equal?` | `inspect`, `to_s`, `class` | | `__id__`, `__send__` | `respond_to?`, `is_a?`, `send` | | `instance_eval`, `instance_exec` | `puts`, `p`, `raise` | | private `initialize`, `method_missing`, `singleton_method_added` and the other singleton-method hooks | `dup`, `clone`, `freeze`, `frozen?` | The double-underscore names `__id__` and `__send__` exist precisely so that code can still identify or message an object whose own `object_id` or `send` is missing or overridden. ## Why subclass BasicObject A near-empty class is useful when **almost every method name should be caught by `method_missing`**: - **Proxies and delegators.** The standard library's `Delegator` (the parent of `SimpleDelegator`) is declared `class Delegator < BasicObject`, so calls like `to_s` or `class` are not answered by the proxy itself but forwarded to the wrapped object. It then mixes back in a trimmed copy of `Kernel` for the few helpers it wants. - **DSL builders.** An object evaluated with `instance_eval` in a configuration DSL can treat any bare word as a setting without colliding with `Kernel` names such as `format`, `test` or `system`. - **Recording or null objects** that must respond to every message. ## The costs of the blank slate 1. **Constants.** Constant references inside such a class do not find top-level constants the usual way; the core docs say to fully qualify them, as in `::String` or `::Kernel`. 2. **No `inspect`.** `p proxy` calls `inspect`, which is missing, so it goes to `method_missing` and, by default, raises `NoMethodError`. 3. **No `raise` or `puts`.** Inside the class, `puts "hi"` is a call to a missing method and raises `NoMethodError`; write `::Kernel.puts "hi"` and `::Kernel.raise`. 4. **Introspection.** Without `respond_to?`, `is_a?` or `class`, tools and debuggers that assume them misbehave; a proxy usually defines the few it needs. ## Why interviewers ask it The question checks that you can name the top of the hierarchy correctly (it is `BasicObject`, not `Object`), that you know `Kernel` is a module rather than a class in the chain, and that you can explain the one practical reason to inherit from `BasicObject`: a blank slate for delegation and DSLs, bought at the price of fully qualified constants and borrowed helpers.

  • Why does Kernel never show up when you follow superclass from Vehicle upwards?
    `Kernel` is a module that `Object` includes, not a class. `Class#superclass` skips modules and returns only the next class, so the walk goes `Vehicle`, `Object`, `BasicObject`, `nil`. Included modules only show up in the full method lookup order, which is a different question.
  • Inside class Proxy < BasicObject, how do you raise an error or print a line?
    Call the `Kernel` module functions with an explicit, fully qualified receiver: `::Kernel.raise ::ArgumentError, "bad"` or `::Kernel.puts "hi"`. A bare `raise` or `puts` is looked up on the proxy, is missing, and ends in `NoMethodError` (or in your own `method_missing`).

BasicObject is an empty workshop with bare walls: Object is the same room with the Kernel toolboard hung up. A proxy wants the bare room, so every request it cannot handle goes straight to the assistant at the door, method_missing.

saying these in an interview costs you the question

  • Object is the root of every Ruby class, so Object.superclass returns nil.
  • Kernel is a class between Object and BasicObject in the superclass chain.
  • A BasicObject subclass still has puts, inspect and respond_to? from Object.
  • BasicObject is abstract, so BasicObject.new raises an error.
  • Subclassing BasicObject is the normal choice for plain domain classes.