Skip to main content
โšก Calmops

Object-Oriented Programming in Ruby: Objects, Classes, and the Object Model

Introduction

Ruby is a purely object-oriented language โ€” everything is an object, including numbers, strings, booleans, and even classes themselves. This article explores Ruby’s object model: what objects and classes really are, how the method lookup chain works, and how this design enables Ruby’s powerful metaprogramming capabilities.

This is the first part of a series. Part 2 will cover modules, mixins, and the full inheritance hierarchy.

What is an Object in Ruby?

In Ruby, an object is essentially two things:

  1. A set of instance variables (its state)
  2. A reference to its class (which holds its methods)
class Dog
  def initialize(name, breed)
    @name  = name   # instance variable
    @breed = breed  # instance variable
  end

  def speak
    "#{@name} says: Woof!"
  end
end

rex = Dog.new("Rex", "Labrador")

rex is an object. It holds @name and @breed as its state. The method speak is not stored in rex โ€” it’s stored in the Dog class. When you call rex.speak, Ruby looks up the method in Dog.

# Inspect an object's instance variables
rex.instance_variables  # => [:@name, :@breed]

# Inspect an object's class
rex.class               # => Dog

# Every object has a unique ID
rex.object_id           # => some integer

What is a Class in Ruby?

Here’s the key insight: a class is also an object. Specifically, a class is an instance of the Class class.

Dog.class    # => Class
Class.class  # => Class  (Class is an instance of itself)

Dog.is_a?(Object)  # => true
Dog.is_a?(Class)   # => true
Dog.is_a?(Module)  # => true  (Class inherits from Module)

A class object holds:

  • A table of instance methods (shared by all instances)
  • A reference to its superclass
Dog.instance_methods(false)  # => [:speak]  (methods defined directly on Dog)
Dog.superclass               # => Object
Object.superclass            # => BasicObject
BasicObject.superclass       # => nil  (top of the hierarchy)

The Method Lookup Chain

When you call a method on an object, Ruby searches for it in a specific order โ€” the ancestor chain:

class Animal
  def breathe
    "breathing..."
  end
end

class Dog < Animal
  def speak
    "Woof!"
  end
end

rex = Dog.new
rex.speak    # found in Dog
rex.breathe  # not in Dog โ†’ found in Animal
rex.class    # not in Dog or Animal โ†’ found in Object
rex.freeze   # found in Object (via Kernel module)

You can inspect the full lookup chain:

Dog.ancestors
# => [Dog, Animal, Object, Kernel, BasicObject]

Ruby walks this list left to right until it finds the method. If it reaches BasicObject without finding it, it calls method_missing.

Classes are Objects: Practical Implications

Because classes are objects, you can:

Store a class in a variable

klass = Dog
obj   = klass.new("Buddy", "Poodle")
obj.speak  # => "Buddy says: Woof!"

Pass a class as an argument

def create_animal(klass, name)
  klass.new(name, "Unknown")
end

create_animal(Dog, "Max").speak  # => "Max says: Woof!"

Define methods on a class dynamically

Dog.define_method(:fetch) do |item|
  "#{@name} fetches the #{item}!"
end

rex.fetch("ball")  # => "Rex fetches the ball!"

Class Methods vs Instance Methods

Instance methods are defined with def inside the class body. Class methods are defined with def self.method_name or inside class << self:

class Dog
  # Instance method โ€” called on an instance
  def speak
    "#{@name} says: Woof!"
  end

  # Class method โ€” called on the class itself
  def self.species
    "Canis lupus familiaris"
  end

  # Alternative: class << self block
  class << self
    def create_puppy(name)
      new(name, "Mixed")
    end
  end
end

rex = Dog.new("Rex", "Lab")
rex.speak          # => "Rex says: Woof!"
Dog.species        # => "Canis lupus familiaris"
Dog.create_puppy("Spot").speak  # => "Spot says: Woof!"

Instance Variables vs Class Variables

class Counter
  # Class variable โ€” shared across ALL instances and subclasses
  @@total_count = 0

  # Class instance variable โ€” belongs to the class object itself
  @default_step = 1

  def initialize
    @@total_count += 1
    @count = 0  # instance variable โ€” unique per instance
  end

  def increment(step = self.class.default_step)
    @count += step
  end

  def count
    @count
  end

  def self.total_count
    @@total_count
  end

  def self.default_step
    @default_step
  end
end

a = Counter.new
b = Counter.new

a.increment
a.increment
b.increment(5)

puts a.count          # => 2
puts b.count          # => 5
puts Counter.total_count  # => 2 (both instances share @@total_count)

Key difference:

  • @@class_variable is shared across the class and all subclasses โ€” behaves like a global variable within the hierarchy
  • @class_instance_variable belongs to the class object itself and is not shared with subclasses
class Animal
  @@count = 0
  @type = "generic"

  def self.count; @@count; end
  def self.type;  @type;   end
end

class Dog < Animal
  @@count = 10  # modifies the SAME @@count as Animal
  @type = "dog" # separate @type โ€” does not affect Animal
end

puts Animal.count  # => 10  (changed by Dog!)
puts Animal.type   # => "generic"  (unchanged)
puts Dog.type      # => "dog"

The Singleton Class

Every object in Ruby has a hidden singleton class (also called eigenclass or metaclass) that can hold methods defined only for that specific object:

rex = Dog.new("Rex", "Lab")

# Define a method only on rex, not on all Dogs
def rex.roll_over
  "#{@name} rolls over!"
end

rex.roll_over          # => "Rex rolls over!"
Dog.new("Spot", "Lab").roll_over  # => NoMethodError

Class methods are actually instance methods of the class’s singleton class:

# These are equivalent:
def Dog.species; "Canis lupus familiaris"; end

class Dog
  class << self  # opens Dog's singleton class
    def species; "Canis lupus familiaris"; end
  end
end

Comparing Ruby’s Object Model to Python and Go

Python

Python also has a rich object model, but with some differences:

# Python: classes are objects too
class Dog:
    def speak(self):
        return "Woof!"

print(type(Dog))    # => <class 'type'>
print(Dog.__mro__)  # Method Resolution Order (like Ruby's ancestors)

Python’s MRO uses the C3 linearization algorithm, similar to Ruby’s ancestor chain.

Go

Go takes a completely different approach โ€” no classes, no inheritance:

// Go: structs + interfaces, no class hierarchy
type Dog struct {
    Name string
}

func (d Dog) Speak() string {
    return d.Name + " says: Woof!"
}

// Interfaces are satisfied implicitly
type Speaker interface {
    Speak() string
}

Go’s simplicity avoids the complexity of deep inheritance hierarchies, at the cost of less flexibility.

Key Takeaways

  • In Ruby, everything is an object โ€” including classes, modules, and nil
  • An object is a set of instance variables + a reference to its class
  • A class is an object (instance of Class) that holds instance methods and a superclass reference
  • Method lookup follows the ancestor chain: class โ†’ superclass โ†’ modules โ†’ Object โ†’ Kernel โ†’ BasicObject
  • Every object has a singleton class for object-specific methods
  • Class methods are instance methods of the class’s singleton class
  • @@class_variables are shared across the hierarchy; @class_instance_variables are per-class

Resources

Comments