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:
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:
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
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:
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:
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.
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."
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
superdoesn't automatically call the parent method unless you actually writesuper(orsuper()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.