Techdots

July 21, 2025

Mastering Ruby's Object Model: Understanding Class, Module, and Singleton Methods

Mastering Ruby's Object Model: Understanding Class, Module, and Singleton Methods

Have you ever wondered what makes Ruby different from other programming languages like Java or Python?

Ruby's object model is unique and powerful, but it can be confusing even for experienced developers. While other languages have strict rules about classes and objects, Ruby gives you incredible flexibility to change how objects behave at runtime.

In this guide, we'll explore Ruby's object model in simple terms. You'll learn about singleton classes, modules, method resolution order, and how Ruby finds the right method when you call it.Β 

By the end, you'll understand why Ruby is so flexible and how to use this power in your own projects. Let’s dive in:Β 

What Are Singleton Classes in Ruby?

Every object in Ruby has a secret companion called a singleton class (also known as an eigenclass). Think of it as a private workspace where Ruby stores methods that belong to just one specific object.

In most programming languages, you can't add a method to just one object - you have to change the entire class. But Ruby lets you do exactly that through singleton classes.

Here's a simple example:


class Person
  def speak
    "Hello, I'm an instance of Person"
  end
end

alice = Person.new
bob   = Person.new

# Define a singleton method on alice
def alice.speak
  "Hi, I'm Alice, and I have my own way of speaking!"
end

puts alice.speak Β  # => "Hi, I'm Alice, and I have my own way of speaking!"
puts bob.speak Β  Β  # => "Hello, I'm an instance of Person"
  

In this example, we gave Alice her own special way of speaking. Ruby created a singleton class just for Alice to hold this new method. Bob still uses the regular Person class method.

You can actually see these singleton classes:


class Person
  def speak
    "Hello, I'm an instance of Person"
  end
end

alice = Person.new
bob   = Person.new

# Define a singleton method on alice
def alice.speak
  "Hi, I'm Alice, and I have my own way of speaking!"
end
  

The output shows that Alice's singleton class sits right above the Person class in the inheritance chain. This means Ruby will check Alice's special methods first, then fall back to the regular Person methods.

Class Methods Are Just Singleton Methods

Here's something that might surprise you: class methods in Ruby are actually singleton methods on the class object itself. Remember, in Ruby, everything is an object - even classes!


class MyClass
end
# Define a class method (singleton method on MyClass)
def MyClass.describe
  "I'm a class method on MyClass"
end
puts MyClass.describe            # => "I'm a class method on MyClass"
puts MyClass.singleton_methods   # => [:describe]
puts MyClass.singleton_class     # => #
  

When you call MyClass.describe, Ruby finds the method in MyClass's singleton class. This is different from languages like Java where static methods are completely separate from instances.

Ruby also gives you another way to define singleton methods using the class << obj syntax:


class Person
  def speak
    "Hello, I'm an instance of Person"
  end
end
alice = Person.new
bob   = Person.new

# Define a singleton method using class << bob syntax
class << bob
  def special_ability
    "Bob's secret skill"
  end
end
puts bob.special_ability   # => "Bob's secret skill"
  

Inside that
class << bob
‍

block, you're actually working inside Bob's singleton class. This is another way to add methods that belong only to Bob.

How Ruby Finds Methods: Method Resolution Order

When you call a method on an object, Ruby doesn't just randomly search for it. It follows a specific order called the Method Resolution Order (MRO). Understanding this order helps you predict which method Ruby will use when there are multiple options.

Here's the order Ruby follows:

  1. The object's singleton class (if it has one)
  2. The object's actual class
  3. Any modules included in that class (in reverse order of inclusion)
  4. The superclass, then any modules in the superclass
  5. Continue up the chain until reaching BasicObject
  6. Call method_missing if nothing is found

Let's see this in action:


module M1
  def greet
    "Hello from M1"
  end
end
module M2
  def greet
    "Hello from M2"
  end
end
class Parent
  def greet
    "Hello from Parent"
  end
end
class Child < Parent
  include M1
  include M2
end
child = Child.new
puts child.greet   # => "Hello from M2"
  

Ruby builds the ancestor chain like this:

  • Child (the class itself)
  • M2 (last included module)
  • M1 (earlier included module)
  • Parent (the superclass)
  • Object, Kernel, BasicObject

When we call obj.greet, Ruby checks Child first, then M2, then M1, and so on. Since M2 was included last, it comes first in the search order. The output will be:

Hello from M2

[Child, M2, M1, Parent, Object, Kernel, BasicObject]

This is simpler than languages with multiple inheritance because Ruby creates a single, linear chain. The first match wins - Ruby doesn't keep searching once it finds a method.

Understanding 'self' in Different Contexts

One of the most confusing aspects of Ruby for beginners is understanding what self refers to. The value of self changes depending on where you are in the code.

Different Contexts of Self

  • Top-level context: self is the main object
  • Inside a class definition: self is the class itself
  • Inside an instance method: self is the instance
  • Inside a class method: self is the class
  • Inside a module definition: self is the module
  • Inside a singleton class: self is the singleton class

Here's an example that shows self in different contexts:


puts "At top level, self = #{self}"   # => main (an instance of Object)
class Demo
  puts "Inside class definition, self = #{self}"   # => Demo (the class)

  def instance_method
    puts "In an instance method, self = #{self}"   # => instance of Demo
  end
  def self.class_method
    puts "In a class method, self = #{self}"       # => Demo (the class)
  end
end

demo = Demo.newdemo.instance_method Β  Β  Β  Β  # => "In an instance method, self = #"

Demo.class_methodΒ  Β  Β  Β  Β  Β  # => "In a class method, self = Demo"

  

Understanding self is crucial because when you call a method without specifying an object, Ruby calls it on self. If you're inside an instance method and call greet, Ruby actually calls self.greet.

Working with Modules and Mixins

Ruby uses modules to share behavior between classes without using inheritance. Think of modules as collections of methods that you can mix into different classes.

What Are Modules?

Modules serve two main purposes:

  1. Namespaces: Grouping related classes and constants
  2. Mixins: Providing methods that can be shared between classes

Here's a practical example using Ruby's Comparable module:


class Card
  include Comparable
  attr_reader :rank
  def initialize(rank)
    @rank = rank
  end
  # Define comparison logic for Comparable
  def <=>(other)
    self.rank <=> other.rank
  end
end
a = Card.new(5)
b = Card.new(10)
puts a < b    # => true  (because 5 < 10)
puts Card.ancestors.inspect
# => [Card, Comparable, Object, Kernel, BasicObject]
  

By including Comparable and defining one method (<=>), Card automatically gets all the comparison methods like <, >, <=, >=, etc.

Include vs Extend

Ruby gives you two ways to use modules:

  • include: Adds the module's methods as instance methods
  • extend: Adds the module's methods as singleton methods

module Loggable
  def log(msg)
    puts "[LOG] #{msg}"
  end
end
class Widget
  include Loggable    # Widget instances get `log` as an instance method
end
widget = Widget.new
widget.log("Hello")   # => [LOG] Hello
# Using `extend` on a single object
other = Object.new
other.extend(Loggable)
other.log("Hi from other")   # => [LOG] Hi from other
  

Prepend vs Include: Changing the Order

Ruby 2.0 introduced prepend, which is similar to include but changes where the module appears in the method lookup chain.

  • include: Puts the module after the class in the lookup chain
  • prepend: Puts the module before the class in the lookup chain

This means prepended modules can override methods in the class itself:


module MixinA
  def hello
    "Hello from MixinA"
  end
end
class Greeter
  def hello
    "Hello from Greeter"
  end
end
# Case 1: include the module
class GreeterIncluded < Greeter
  include MixinA
end
# Case 2: prepend the module
class GreeterPrepended < Greeter
  prepend MixinA
end
puts GreeterIncluded.new.hello    # => "Hello from Greeter"
puts GreeterPrepended.new.hello   # => "Hello from MixinA"
  

Prepend is useful when you want to wrap or modify existing behavior:


module BeforeAndAfter
  def hello
    puts "Before hello"
    result = super   # Calls the original `hello` from Foo
    puts "After hello"
    result
  end
end
class Foo
  def hello
    "Hello from Foo"
  end
end
# Prepend the module to Foo
class Foo
  prepend BeforeAndAfter
end
puts Foo.new.hello
  

How Inheritance Works with Class Methods

Ruby's approach to class method inheritance is unique. In many languages, static methods aren't truly inherited or polymorphic. But in Ruby, class methods are just singleton methods on class objects, so they inherit normally.


class Animal
  def self.speak
    "Animal sound"
  end
end
class Dog < Animal
end
class Cat < Animal
  def self.speak
    "Meow!"
  end
end
puts Dog.speak       # => "Animal sound"  (inherited from Animal)
puts Cat.speak       # => "Meow!"         (Cat overrode the class method)
puts Animal.speak    # => "Animal sound"
  

Dog didn't define its own speak method, so it uses the one from Animal. Cat overrode it with its own version. This works because Dog's singleton class inherits from Animal's singleton class.

Advanced Method Lookup Techniques

Ruby provides several advanced features for handling method calls and defining methods dynamically.

Method Missing

When Ruby can't find a method, it calls method_missing as a last resort. You can override this to handle undefined methods:


class Ghost
  def method_missing(name, *args, &block)
    puts "β†’ Got a call to #{name}(#{args.join(', ')}) but no such method exists."
  end
end
g = Ghost.new
g.shriek("loudly")
  

If you override method_missing, you should also override respond_to_missing? to keep introspection working:


class Ghost
  def method_missing(name, *args, &block)
    if name.to_s.start_with?("boo_")
      "πŸ‘» Boo says #{name.to_s.sub('boo_', '')}!"
    else
      super
    end
  end
  def respond_to_missing?(name, include_private = false)
    name.to_s.start_with?("boo_") || super
  end
end
g = Ghost.new
puts g.boo_haha               # => "πŸ‘» Boo says haha!"
puts g.respond_to?(:boo_haha) # => true
  

Defining Methods Dynamically

Ruby lets you create methods at runtime:


class Person
  # Dynamically create a bunch of setter methods
  ["name", "age", "location"].each do |prop|
    define_method("#{prop}=") do |value|
      instance_variable_set("@#{prop}", value)
    end
  end
end
p = Person.new
p.name = "Alice"
p.age = 30
p.location = "Wonderland"
  

You can also add methods to individual objects:


str = "I'm a string"
str.define_singleton_method(:welcome) do
  "#{self} says welcome!"
end
puts str.welcome   # => "I'm a string says welcome!"
  

Using Send for Method Calls

The send method lets you call methods by name, even private ones:


class Secret
  private

  def whisper
    "psst"
  end
end
s = Secret.new
# s.whisper would raise an error (private method), but:
puts s.send(:whisper)    # => "psst"
  

Best Practices for Ruby Object Model

Understanding Ruby's object model helps you write better, more maintainable code. Here are some key practices:

Use Modules for Shared Behavior

When multiple unrelated classes need similar functionality, modules are often better than inheritance:


module Taggable
  def add_tag(tag)
    @tags ||= []
    @tags << tag
  end
  
  def tags
    @tags || []
  end
end
class User
  include Taggable
end
class Product
  include Taggable
end
  

Save Inheritance for "Is-A" Relationships

Use inheritance when there's a clear hierarchy:


class User
  def initialize(name)
    @name = name
  end
end
class AdminUser < User
  def admin?
    true
  end
end
  

Be Careful with Method Missing

While method_missing is powerful, overusing it can make code hard to debug. When possible, generate real methods using define_method:


# Better: generate methods when you know them
class DynamicClass
  ["foo", "bar", "baz"].each do |name|
    define_method(name) do
      "Called #{name}"
    end
  end
end
# These methods show up in obj.methods
puts DynamicClass.new.methods.grep(/foo|bar|baz/)
  

Understand Self Context

Always be aware of what self refers to in your current context. This prevents bugs, especially when defining methods or using attr_accessor:


class MyClass
  # Here, self refers to the class itself (MyClass)
  puts "In class definition: #{self}"

  def instance_method
    # Here, self refers to the instance of MyClass
    puts "In instance method: #{self}"
  end
  def self.class_method
    # Here, self refers to the class (MyClass)
    puts "In class method: #{self}"
  end
end
  

Use Introspection for Debugging

Ruby provides tools to understand what's happening:


# Find out what methods an object has
obj.methods             # => All public/protected methods available to obj
obj.singleton_methods   # => Only methods defined on obj's singleton class
# See the inheritance chain (method lookup path)
obj.class.ancestors     # => Array of modules/classes Ruby will search
# Find where a method is defined (file + line number)
obj.method(:some_method).source_location
# => ["/path/to/file.rb", line_number] or nil for built-ins
  

Conclusion

Ruby's object model gives you incredible flexibility to write expressive, dynamic code.Β 

By understanding singleton classes, method resolution order, modules, and the behavior of self, you can harness Ruby's power effectively.Β 

Remember to use these features wisely - with great power comes great responsibility to write clear, maintainable code.

Ready to dive deeper into Ruby development? At TechDots, we specialize in Ruby on Rails development and can help you build robust, scalable applications.Β 

Contact us today to discuss your next project!

Ready to Launch Your AI MVP with Techdots?

Techdots has helped 15+ founders transform their visions into market-ready AI products. Each started exactly where you are now - with an idea and the courage to act on it.

Techdots: Where Founder Vision Meets AI Reality

Book Meeting