Skip to Content
Course content

120: Common Ruby Interview Questions on Object Model

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this happen more times than I can count during code reviews. A developer wants to "patch" a single object's behavior without affecting every other instance of that class—maybe for a mock in a test or a specialized configuration object. They write something that looks like it should work, but then they get completely lost when they try to debug where that method actually lives.

class Report
  def generate
    "Standard Report"
  end
end

monthly_report = Report.new

# I only want this specific instance to have a different greeting
def monthly_report.generate
  "Monthly Special Report"
end

puts monthly_report.generate # => "Monthly Special Report" (Works!)
puts Report.new.generate     # => "Standard Report" (Correct, didn't break other reports)

# Now, the developer tries to find where this method is defined for logging:
puts Report.instance_methods.include?(:generate) # => true
# But wait, why isn't "Monthly Special Report" in the class definition?
# And where exactly did that specific override go?

The Invisible Method Mystery

The developer in this scenario is confused because they've defined a method on monthly_report, but the method doesn't exist in the Report class. In many languages, you can't just "add a method" to a single instance. In Ruby, you can, but it creates a hidden layer in the object model that often trips people up during interviews because it's not immediately visible.

When you call def monthly_report.generate, Ruby doesn't modify the Report class. Instead, it creates a singleton class (also known as an eigenclass) for that specific object. This is an anonymous class that sits between the object and its actual class in the lookup chain.

Exposing the Singleton Class

To fix the confusion and actually see what's happening, you have to stop looking at the class and start looking at the singleton class. You can access it using the singleton_class method.

# Instead of checking the class:
puts Report.instance_methods.include?(:generate) 

# Check the singleton class of the specific instance:
puts monthly_report.singleton_class.instance_methods.include?(:generate) # => true

# You can even see where it sits in the ancestors chain:
pp monthly_report.singleton_class.ancestors
# [#<Class:#<Report:0x000...>>, Report, Object, Kernel, BasicObject]

The fix here isn't changing the code—the code worked—but changing the mental model. The "bug" was a failure to understand that monthly_report now has its own private class that inherits from Report. When Ruby looks for a method, it checks the singleton class first, then the actual class, then the modules, and so on.

Navigating the Ancestors Chain

In an interview, you'll likely be asked about the "method lookup path." If I ask you, "What happens when you call a method on an object?", don't just say "it looks in the class." That's the junior answer. The senior answer is: "Ruby checks the singleton class, then any included modules in reverse order of inclusion, then the class itself, then the superclass chain, ending at BasicObject."

I always suggest remembering the chain like this: Object → Singleton Class → Modules → Class → Superclass → BasicObject. If Ruby hits BasicObject and still hasn't found the method, it triggers method_missing.

The 'method_missing' Safety Net

Interviewers love method_missing because it's the foundation of "magic" in Ruby (like how ActiveRecord handles find_by_username). But it's a double-edged sword. If you override method_missing, you must call super if you don't handle the method, or you'll break the entire object model and get confusing errors.

class Ghost
  def method_missing(method_name, *args, &block)
    if method_name.to_s.start_with?("say_")
      puts "You told me to say: #{method_name.to_s.sub('say_', '')}"
    else
      # Crucial: allow the method to fail naturally if we don't handle it
      super 
    end
  end

  # Pro tip: Always override respond_to_missing? when using method_missing
  def respond_to_missing?(method_name, include_private = false)
    method_name.to_s.start_with?("say_") || super
  end
end

g = Ghost.new
g.say_hello # => "You told me to say: hello"
g.dance     # => NoMethodError (because we called super)

Include vs Extend

This is a classic "gotcha" question. I tell my mentees to think of it this way: include adds methods to the instances of the class, while extend adds methods to the singleton class of the object (or the class object itself).

  • Include: The module's methods become instance methods.
  • Extend: The module's methods become "class methods" (because the class is just an object, and extend puts methods in its singleton class).



📋 Practical Task

The Dynamic Proxy Debugger

You are building a proxy object that logs every method call it receives before delegating the call to a real "target" object. However, your current implementation is crashing when it encounters methods it doesn't recognize, and it's failing a test that checks if the proxy "responds" to the target's methods.

Your Goal: Fix the Proxy class so that:

  1. It logs "Calling method: [method_name]" whenever any method is called.
  2. It successfully forwards the call to the @target object.
  3. It correctly handles methods that @target does not possess (by letting Ruby raise the standard NoMethodError).
  4. proxy.respond_to?(:some_method) returns true if the @target responds to it.
class Proxy
  def initialize(target)
    @target = target
  end

  def method_missing(method_name, *args, &block)
    puts "Calling method: #{method_name}"
    # TODO: Fix the delegation and error handling here
  end

  # TODO: Implement respond_to_missing?
end

# Test Case
class Calculator
  def add(a, b); a + b; end
end

calc = Calculator.new
proxy = Proxy.new(calc)

puts proxy.add(5, 10)      # Should print "Calling method: add" then "15"
puts proxy.respond_to?(:add) # Should be true
puts proxy.subtract(5, 10)   # Should raise NoMethodError
Rating
0 0

There are no comments for now.

to be the first to leave a comment.