OOP in Ruby

Classes, attr_accessor, inheritance, modules and mixins, and the duck typing philosophy.

Classes

Every value in Ruby is an object, and every object is an instance of a class:

Ruby
class Car
  def initialize(model)   # the constructor — called automatically by Car.new
    @model = model         # @model is an instance variable — this object's state
    @speed = 0
  end

  def accelerate(amount)
    @speed += amount
  end

  def describe
    "#{@model} is going #{@speed} km/h"
  end
end

car = Car.new("Civic")
car.accelerate(40)
puts car.describe   # Civic is going 40 km/h

attr_accessor, attr_reader, attr_writer

Writing a getter and setter method by hand for every instance variable is repetitive, so Ruby provides a shorthand that generates them for you:

Ruby
class Car
  attr_accessor :model, :speed   # generates both a reader and a writer method

  def initialize(model)
    @model = model
    @speed = 0
  end
end

car = Car.new("Civic")
car.speed = 40        # uses the generated writer (setter)
puts car.speed         # 40 — uses the generated reader (getter)
  • attr_accessor — generates both a getter and setter.
  • attr_reader — generates only a getter (read-only from outside the class).
  • attr_writer — generates only a setter (rare — write-only attributes are unusual).

Inheritance

Ruby
class Vehicle
  attr_reader :speed

  def initialize
    @speed = 0
  end

  def accelerate
    @speed += 10
  end
end

class SportsCar < Vehicle
  def accelerate
    @speed += 30   # overrides the parent's method entirely
  end
end

car = SportsCar.new
car.accelerate
puts car.speed   # 30

class Truck < Vehicle
  def accelerate
    super          # calls Vehicle#accelerate first...
    @speed -= 2     # ...then adjusts the result — trucks accelerate a bit slower
  end
end

truck = Truck.new
truck.accelerate
puts truck.speed   # 8

super calls the parent class's version of the current method — with no arguments and no parentheses, it automatically forwards all the arguments the current method received.

Ruby only supports single inheritance — a class can have exactly one superclass — which is precisely the gap modules are designed to fill.

Modules and mixins

A module groups related methods and constants together, but — unlike a class — can never be instantiated on its own. Its main job is to be mixed in to one or more classes with include, sharing behavior without needing (single) inheritance:

Ruby
module Flyable
  def fly
    "#{self.class.name} is flying!"
  end
end

module Swimmable
  def swim
    "#{self.class.name} is swimming!"
  end
end

class Duck
  include Flyable
  include Swimmable
end

duck = Duck.new
puts duck.fly    # Duck is flying!
puts duck.swim   # Duck is swimming!

A class can include any number of modules — this is Ruby's answer to the "single inheritance isn't enough" problem that interfaces solve in Java and traits solve in other languages. Under the hood, include inserts the module into the class's method lookup chain (its "ancestors"), which you can inspect directly:

Ruby
puts Duck.ancestors
# [Duck, Swimmable, Flyable, Object, Kernel, BasicObject]

extend is the related sibling: include mixes a module's methods in as instance methods, while extend mixes them in as methods on a single object or as class-level methods.

Ruby
module Greetable
  def greet
    "Hello from #{self}"
  end
end

class Report
  extend Greetable   # adds `greet` as a class method, not an instance method
end

puts Report.greet   # Hello from Report

Duck typing

Ruby doesn't check an object's class before calling a method on it — it just tries to call the method, and if the object responds to it, the call succeeds. This is duck typing: "if it walks like a duck and quacks like a duck, it's a duck."

Ruby
class Duck
  def speak
    "Quack!"
  end
end

class Dog
  def speak
    "Woof!"
  end
end

def make_it_speak(animal)
  puts animal.speak   # works on ANY object that responds to #speak — no shared parent required
end

make_it_speak(Duck.new)  # Quack!
make_it_speak(Dog.new)   # Woof!

There's no interface declaration, no shared parent class required — make_it_speak doesn't care what class animal is, only that it responds to .speak. This is central to how idiomatic Ruby code is written, and why the community favors testing behavior over checking is_a?/class explicitly.

Common mistakes

  • Reaching for deep inheritance hierarchies to share behavior when a module mixin is a better, flatter fit — Ruby culture strongly favors composition via modules.
  • Forgetting super doesn't automatically call the parent method unless you actually write super (or super() to explicitly pass no arguments) — omitting it entirely skips the parent's behavior.
  • Checking obj.is_a?(SomeClass) everywhere instead of just calling the method and trusting duck typing — this defeats much of the flexibility Ruby's dynamic dispatch offers.

Interview questions

Q: What's the difference between a class and a module in Ruby? A class can be instantiated (SomeClass.new) and supports (single) inheritance. A module can never be instantiated directly — its purpose is to group reusable methods/constants and be mixed into one or more classes via include or extend, which is how Ruby achieves multiple-inheritance-like code sharing.

Q: What is duck typing, and how does it change how you'd design a method's parameters? Duck typing means Ruby cares whether an object responds to a given method, not what class it belongs to. Practically, this means you design methods around the behavior (methods) an argument must support, not a specific required class or interface — any object that "quacks" the right way works, without formally declaring it implements anything.

Q: What does include do, technically? It inserts the module into the including class's method resolution order ("ancestors" chain), directly above the class itself, so instances of that class can call the module's methods as if they were defined on the class — you can see this yourself by calling SomeClass.ancestors.