Skip to content

07 · Classes & Objects Basics

Defining a class

class Person
  def initialize(name, age)   # the constructor
    @name = name                # @ prefix = instance variable
    @age = age
  end

  def greet
    "Hi, I'm #{@name} and I'm #{@age} years old."
  end
end

ada = Person.new("Ada", 30)
puts ada.greet   # Hi, I'm Ada and I'm 30 years old.

initialize is the method Ruby calls automatically when you write Person.new(...) — it's the constructor, but it's just a regular method under the hood.

Instance variables need explicit accessor methods

class Person
  def initialize(name, age)
    @name = name
    @age = age
  end

  def name
    @name
  end

  def age=(new_age)   # a "setter" method -- Ruby lets you call it as ada.age = 31
    @age = new_age
  end
end

ada = Person.new("Ada", 30)
puts ada.name    # Ada
# ada.age        # error: private method `age' called -- no getter defined for age!
ada.age = 31       # this works -- age=(new_age) was defined

Instance variables (@name) are not accessible from outside the class unless you define methods that expose them — there's no automatic public field access like in Python or JavaScript.

attr_accessor, attr_reader, attr_writer

class Person
  attr_accessor :name    # generates both a getter AND setter for :name
  attr_reader :age         # generates only a getter for :age

  def initialize(name, age)
    @name = name
    @age = age
  end
end

ada = Person.new("Ada", 30)
puts ada.name     # Ada       -- getter, from attr_accessor
ada.name = "Ada L." # works    -- setter, from attr_accessor
puts ada.age       # 30        -- getter, from attr_reader
# ada.age = 31     # error: no setter -- attr_reader only generates a getter

attr_accessor, attr_reader, and attr_writer are the idiomatic shortcut for the boilerplate getter/setter methods shown above — almost every Ruby class uses one of these instead of writing them by hand.

Class methods vs instance methods

class Person
  attr_reader :name

  def initialize(name)
    @name = name
  end

  # `self.` prefix makes this a CLASS method, called as Person.count_greeting
  def self.default
    new("Anonymous")
  end

  def greet
    "Hi, I'm #{@name}"
  end
end

anon = Person.default
puts anon.greet   # Hi, I'm Anonymous

Inheritance

class Animal
  def initialize(name)
    @name = name
  end

  def speak
    "..."
  end
end

class Dog < Animal
  def speak
    "#{@name} says Woof!"
  end
end

class Cat < Animal
  def speak
    "#{@name} says Meow!"
  end
end

[Dog.new("Rex"), Cat.new("Whiskers")].each do |animal|
  puts animal.speak
end
# Rex says Woof!
# Whiskers says Meow!

super — calling the parent's version

class Animal
  def initialize(name)
    @name = name
  end
end

class Dog < Animal
  def initialize(name, breed)
    super(name)   # calls Animal#initialize with `name`
    @breed = breed
  end

  def describe
    "#{@name} is a #{@breed}"
  end
end

rex = Dog.new("Rex", "Labrador")
puts rex.describe   # Rex is a Labrador

How It Actually Works

Every object in Ruby carries an invisible pointer to its class, and every class is itself an object — an instance of Class, which is itself an instance of Class. When you write class Dog; end, MRI allocates a Class object and stores a method table (a hash from method name to bytecode/C-function pointer) directly on it. Calling dog.bark doesn't search dog itself for a bark method — instance variables and methods live in different places: @name is stored in a slot table on the instance, while bark's implementation lives in the class's method table, found via the ancestor-chain walk described elsewhere in this site. attr_accessor :name is just Ruby code that defines two methods (name and name=) on the class at load time using define_method internally — there's no special compiler magic, you could write those two defs by hand and get identical behavior.

Cheat sheet

Feature Syntax
Define a class class Name ... end
Constructor def initialize(args) ... end
Instance variable @name
Getter + setter attr_accessor :name
Getter only attr_reader :name
Class method def self.method_name ... end
Inheritance class Sub < Base ... end
Call parent method super(args)

🔀 See this in another language

Exercise

Write a class BankAccount with attr_reader :owner, :balance, an initialize(owner, balance), and methods deposit(amount) and withdraw(amount) (the latter should return "Insufficient funds" as a string instead of actually withdrawing if the amount exceeds the balance). Then write a subclass SavingsAccount < BankAccount that adds an interest_rate and a method apply_interest that increases the balance by that rate.