Skip to content

Object-Oriented Programming | Ruby

## Basic class
class Person
def initialize(name, age)
@name = name
@age = age
end
def name
@name
end
def age
@age
end
def greet
"Hello, I"m #{@name}, age #{@age}"
end
end
alice = Person.new("Alice", 30)
puts alice.greet # => "Hello, I'm Alice, age 30"
## Classes are first-class objects
Person.class # => Class
Person.superclass # => Object
Person.ancestors # => [Person, Object, Kernel, BasicObject]
# Class names are constants
Person = Class.new do
def initialize(name)
@name = name
end
def to_s
@name
end
end
class Book
# attr_reader: generates getter methods
attr_reader :title, :author
# attr_writer: generates setter methods
attr_writer :price
# attr_accessor: generates both getter and setter
attr_accessor :isbn, :published_year
def initialize(title, author, price)
@title = title
@author = author
@price = price
@isbn = nil
@published_year = nil
end
# Custom getter with computation
def price_with_tax(rate = 0.1)
@price * (1 + rate)
end
# Custom setter with validation
def price=(new_price)
raise ArgumentError, "Price must be positive" unless new_price > 0
@price = new_price
end
end
book = Book.new("Ruby Guide", "Matz", 39.99)
book.title # => "Ruby Guide"
book.price = 49.99 # calls the custom setter
book.isbn = "978-123" # generated setter
class Point
def initialize(x = 0, y = 0)
@x = x
@y = y
end
end
Point.new(3, 4) # creates a new Point instance
# initialize is a private method
Point.instance_method(:initialize).name # => :initialize
# allocate creates an instance without calling initialize
raw = Point.allocate
raw.instance_variables # => [] (no @x, @y set)
# Overriding new for custom object creation
class Singleton
@@instance = nil
def self.new(*args, &block)
raise "Use Singleton.instance" if @@instance
super
end
def self.instance
@@instance ||= new
end
private_class_method :new
end
# Base class
class Animal
attr_accessor :name, :age
def initialize(name, age)
@name = name
@age = age
end
def speak
"#{name} makes a sound"
end
def to_s
"#{name} (#{self.class})"
end
end
# Subclass with super
class Dog < Animal
attr_accessor :breed
def initialize(name, age, breed)
super(name, age) # calls Animal#initialize
@breed = breed
end
def speak
"#{name} barks!" # overrides Animal#speak
end
# Call parent method with super
def info
"#{super} -- #{breed}"
end
end
class Cat < Animal
def speak
"#{name} meows!"
end
end
rex = Dog.new("Rex", 5, "Labrador")
puts rex.speak # => "Rex barks!"
puts rex.info # => "Rex (Dog) -- Labrador"
# super behaviour
class Base
def greet
"Hello"
end
end
class Child < Base
def greet
super + " from Child" # passes args to parent
super() # calls parent with no args
end
end
# Method resolution order (MRO)
class A; end
class B < A; end
class C < A; end
class D < B; end
D.ancestors # => [D, B, A, Object, Kernel, BasicObject]

Modules serve two purposes: namespaces and mixins.

module MathEngine
PI = 3.141592653589793
def self.circle_area(radius)
PI * radius ** 2
end
def self.circle_circumference(radius)
2 * PI * radius
end
class Vector2D
def initialize(x, y)
@x = x
@y = y
end
def magnitude
Math.sqrt(@x ** 2 + @y ** 2)
end
end
end
MathEngine.circle_area(5) # => 78.5398...
MathEngine::PI # => 3.14159...
v = MathEngine::Vector2D.new(3, 4)
v.magnitude # => 5.0

Modules as Mixins (include / extend / prepend)

Section titled “Modules as Mixins (include / extend / prepend)”
# A module with instance methods
module Validation
def validate!
raise "Invalid state" unless valid?
end
def valid?
true
end
end
# include: adds instance methods
class User
include Validation
def initialize(name, email)
@name = name
@email = email
end
def valid?
!@name.nil? && !@email.nil? && @email.include?("@")
end
end
user = User.new("Alice", "alice@example.com")
user.valid? # => true
user.validate! # no error
# extend: adds methods as singleton methods (class-level on instance)
class Config
extend Validation
end
Config.valid? # => true
Config.validate! # no error
# prepend: adds methods before the class in the lookup chain
module Logging
def save
puts "Before save: #{self.inspect}"
super
puts "After save: #{self.inspect}"
end
end
class Document
prepend Logging
def save
puts "Saving document"
end
end
doc = Document.new
doc.save
# => "Before save: #<Document:...>"
# => "Saving document"
# => "After save: #<Document:...>"
module A
def hello
"A#hello"
end
end
module B
def hello
"B#hello"
end
end
class Example
include A
include B
end
Example.ancestors
# => [Example, B, A, Object, Kernel, BasicObject]
# include adds to ancestors chain (last included appears first in lookup)
Example.new.hello # => "B#hello"
class Example2
include A
prepend B
end
Example2.ancestors
# => [B, Example2, A, Object, Kernel, BasicObject]
Example2.new.hello # => "B#hello"
# Class-level extend vs include
class Klass
include M # instance methods from M
end
class Klass
extend M # class methods from M
end
# module_function: methods become both instance and module methods
module Utilities
def factorial(n)
n <= 1 ? 1 : n * factorial(n - 1)
end
module_function :factorial
# module_function without argument affects all subsequent methods
module_function
def fibonacci(n)
return n if n <= 1
fibonacci(n - 1) + fibonacci(n - 2)
end
end
Utilities.factorial(5) # => 120
Utilities.fibonacci(10) # => 55
# Included in a class
class Calculator
include Utilities
end
calc = Calculator.new
calc.factorial(5) # => 120 (available as instance method)
class User
@@count = 0
def initialize(name)
@name = name
@@count += 1
end
# Class method with self.
def self.count
@@count
end
def self.find_by_name(name)
# Database lookup simulation
all_users.find { |u| u.name == name }
end
def self.all_users
@users ||= []
end
# Alternative syntax: class << self block
class << self
def search(query)
all_users.select { |u| u.name.include?(query) }
end
def reset!
@users = []
end
end
end
User.count # => 0
alice = User.new("Alice")
User.count # => 1
# Class methods are singleton methods on the class object
User.singleton_methods # => [:count, :find_by_name, :all_users, :search, :reset!]
class Parent
@@family = "shared"
def self.family
@@family
end
end
class Child < Parent
@@family = "overridden"
end
Parent.family # => "overridden" -- class variables are shared across hierarchy!
# Safer alternative: class instance variables
class SafeParent
@family = "parent default"
class << self
attr_accessor :family
end
end
class SafeChild < SafeParent
@family = "child default"
end
SafeParent.family # => "parent default"
SafeChild.family # => "child default" -- not shared!
# Instance variables belong to a specific instance
class Counter
def initialize
@count = 0
end
def increment
@count += 1
end
def count
@count
end
end
c1 = Counter.new
c2 = Counter.new
c1.increment
c1.increment
c2.increment
c1.count # => 2
c2.count # => 1 (independent instances)
class BankAccount
attr_reader :balance
def initialize(owner, initial_balance = 0)
@owner = owner
@balance = initial_balance
@transactions = []
end
# Public by default
def deposit(amount)
raise ArgumentError, "Amount must be positive" unless amount > 0
@balance += amount
record_transaction(:deposit, amount)
end
def withdraw(amount)
raise ArgumentError, "Amount must be positive" unless amount > 0
raise ArgumentError, "Insufficient funds" if amount > @balance
@balance -= amount
record_transaction(:withdrawal, amount)
end
# Protected: accessible to instances of the same class and subclasses
protected
def record_transaction(type, amount)
@transactions << { type: type, amount: amount, balance: @balance }
end
# Private: only accessible within the instance (no explicit receiver)
private
def validate_amount(amount)
amount > 0 && amount <= @balance
end
end
class SavingsAccount < BankAccount
def transfer(other_account, amount)
withdraw(amount)
other_account.deposit(amount)
end
def compare_balance(other)
# Protected methods can be called on other instances of the same class
if @balance > other.balance
"Higher balance"
else
"Lower or equal balance"
end
end
end
# Private methods cannot have an explicit receiver
# account.validate_amount(100) # => NoMethodError (private)
# self.validate_amount(100) # => NoMethodError (private)
# validate_amount(100) # => works (implicit self)
class Example
def public_method; end # public
protected
def protected_method; end # protected
private
def private_method; end # private
public
def another_public; end # back to public
end
# Per-method visibility
class Example2
def method_a; end
def method_b; end
private :method_b
def method_c; end
end

Including Comparable and implementing <=> gives you access to comparison operators:

class Version
include Comparable
attr_reader :major, :minor, :patch
def initialize(major, minor = 0, patch = 0)
@major = major
@minor = minor
@patch = patch
end
def <=>(other)
comparison = @major <=> other.major
return comparison unless comparison.zero?
comparison = @minor <=> other.minor
return comparison unless comparison.zero?
@patch <=> other.patch
end
def to_s
"#{major}.#{minor}.#{patch}"
end
def hash
[major, minor, patch].hash
end
def eql?(other)
self == other
end
end
v1 = Version.new(1, 2, 3)
v2 = Version.new(1, 2, 10)
v3 = Version.new(2, 0, 0)
v1 < v2 # => true
v1 == v2 # => false
v2 < v3 # => true
v1 <=> v2 # => -1
v1.between?(v2, v3) # => true (from Comparable)
[v2, v1, v3].sort.map(&:to_s) # => ["1.2.3", "1.2.10", "2.0.0"]

Including Enumerable and implementing each provides dozens of iteration methods:

class WordList
include Enumerable
def initialize
@words = []
end
def add(word)
@words << word.downcase
self
end
def each
return enum_for(__method__) unless block_given?
@words.each { |word| yield word }
end
def size
@words.size
end
end
wl = WordList.new
wl.add("Ruby").add("Python").add("JavaScript").add("Ruby")
# All Enumerable methods available
wl.map(&:upcase) # => ["RUBY", "PYTHON", "JAVASCRIPT", "RUBY"]
wl.select { |w| w.length > 4 } # => ["python", "javascript"]
wl.reject { |w| w.start_with?("r") } # => ["python", "javascript"]
wl.count # => 4
wl.uniq # => ["ruby", "python", "javascript"]
wl.sort # => ["javascript", "python", "ruby"]
wl.any? { |w| w.length > 10 } # => false
wl.all? { |w| w.length > 2 } # => true
wl.find { |w| w == "ruby" } # => "ruby"
wl.group_by { |w| w.length } # => {4=>["ruby"], 6=>["python"], 10=>["javascript"]}
wl.reduce(:+) # => "rubypythonjavascriptruby"
wl.min # => "javascript"
wl.max # => "ruby"
wl.minmax # => ["javascript", "ruby"]
wl.first(2) # => ["ruby", "python"]
wl.member?("python") # => true
# Custom collection with lazy support
class FibonacciSequence
include Enumerable
def initialize(limit)
@limit = limit
end
def each
a, b = 0, 1
@limit.times do
yield a
a, b = b, a + b
end
end
end
seq = FibonacciSequence.new(10)
seq.to_a # => [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
seq.select(&:even?) # => [0, 2, 8, 34]
seq.sum # => 88
class Engine
def initialize(horsepower)
@horsepower = horsepower
end
def start
puts "Engine started (#{@horsepower} HP)"
end
def stop
puts "Engine stopped"
end
end
class GPS
def navigate(destination)
puts "Navigating to #{destination}"
end
end
class Car
def initialize(horsepower)
@engine = Engine.new(horsepower)
@gps = GPS.new
end
def start
@engine.start
end
def navigate(destination)
@gps.navigate(destination)
end
end
car = Car.new(200)
car.start
car.navigate("London")
require 'singleton'
class DatabaseConnection
include Singleton
def connect
@connected = true
puts "Connected to database"
end
def query(sql)
raise "Not connected" unless @connected
puts "Executing: #{sql}"
end
private
def initialize
@connected = false
end
end
db = DatabaseConnection.instance
db.connect
db.query("SELECT * FROM users")
module Observable
def initialize
@observers = []
end
def add_observer(observer)
@observers << observer
end
def remove_observer(observer)
@observers.delete(observer)
end
def notify_observers(*args)
@observers.each { |observer| observer.update(*args) }
end
end
class EventPublisher
include Observable
def publish(event)
puts "Publishing: #{event}"
notify_observers(event)
end
end
class Logger
def update(event)
puts "[LOG] #{event}"
end
end
class Metrics
def update(event)
puts "[METRICS] event recorded"
end
end
publisher = EventPublisher.new
publisher.add_observer(Logger.new)
publisher.add_observer(Metrics.new)
publisher.publish("user_signed_up")
# Struct: lightweight class creation
Person = Struct.new(:name, :email, :age) do
def adult?
age >= 18
end
end
p = Person.new("Alice", "a@b.com", 30)
p.name # => "Alice"
p.adult? # => true
p[:email] # => "a@b.com"
p.to_a # => ["Alice", "a@b.com", 30]
p.to_h # => {name: "Alice", email: "a@b.com", age: 30}
# Struct with keyword_init (Ruby 2.5+)
Person = Struct.new(:name, :email, :age, keyword_init: true)
p = Person.new(name: "Bob", age: 25)
p.name # => "Bob"
# OpenStruct: flexible hash-like object
require 'ostruct'
config = OpenStruct.new(host: "localhost", port: 8080)
config.host # => "localhost"
config.port # => 8080
config.timeout = 30 # dynamically add fields
config.timeout # => 30
# Data: immutable value objects
class Point < Data
params :x, :y
end
p = Point.new(3, 4)
p.x # => 3
p.y # => 4
p.frozen? # => true
p == Point.new(3, 4) # => true (value equality)
module M1
def greet; puts "M1#greet"; end
end
module M2
def greet; puts "M2#greet"; end
end
class Base
def greet; puts "Base#greet"; super; end
end
class Child < Base
include M1
include M2
def greet; puts "Child#greet"; super; end
end
Child.ancestors
# => [Child, M2, M1, Base, Object, Kernel, BasicObject]
Child.new.greet
# => "Child#greet"
# => "M2#greet"
# => "M1#greet"
# => "Base#greet"
# (Base calls super, goes to Object, then Kernel, no more greet methods)
flowchart TD
    A[1_Oop] --> B[Key Concepts]
    A --> C[Core Principles]
    A --> D[Practical Applications]
    B --> E[Fundamental definitions]
    C --> F[Design patterns]
    D --> G[Real-world usage]

Ruby’s OOP is like a hierarchy of roles in a theater. Classes are the scripts that define what a character can say and do. Objects are the actors who play those roles. Inheritance is like a understudy who learns from the lead actor: the understudy can do everything the lead can, plus their own special moves.

Mixins through modules are like costume changes. A single actor (object) can wear different costumes (mix modules) to take on different roles. The same person might be a detective in one scene and a chef in another, gaining different abilities through each costume.

Example 1: Mixin-based Authentication System

Section titled “Example 1: Mixin-based Authentication System”

Problem: Build an authentication concern that can be mixed into multiple models, providing authenticate, current_user, and token generation.

module Authenticatable
extend ActiveSupport::Concern
included do
has_many :sessions, dependent: :destroy
has_secure_password
end
def generate_token
token = SecureRandom.urlsafe_base64(32)
sessions.create!(token: token, expires_at: 30.days.from_now)
token
end
def authenticate_with_token(token)
session = sessions.find_by(token: token)
return nil unless session && !session.expired?
session.update!(last_used_at: Time.current)
self
end
def logout(token)
sessions.find_by(token: token)&.destroy
end
module ClassMethods
def authenticate(email:, password:)
user = find_by(email: email)
user&.authenticate(password)
end
end
end
class User < ApplicationRecord
include Authenticatable
validates :email, presence: true, uniqueness: true
end
class Admin < ApplicationRecord
include Authenticatable
validates :role, inclusion: { in: %w[superadmin admin editor] }
end

Explanation: ActiveSupport::Concern provides a clean way to organize mixins. The included block runs when the module is included, setting up associations and has_secure_password. ClassMethods extends the class with authenticate. Both User and Admin gain authentication without inheritance.


Problem: Implement a payment processing system where different payment strategies (credit card, PayPal, bank transfer) are encapsulated in separate modules.

module PaymentStrategies
module CreditCard
def process_payment(amount)
puts "Charging $#{amount} to credit card #{card_number[-4..]}"
{ success: true, reference: "CC-#{rand(1000..9999)}" }
end
def refund(reference, amount)
puts "Refunding $#{amount} for reference #{reference}"
{ success: true }
end
end
module PayPal
def process_payment(amount)
puts "Processing $#{amount} via PayPal for #{paypal_email}"
{ success: true, reference: "PP-#{rand(1000..9999)}" }
end
def refund(reference, amount)
puts "Refunding $#{amount} via PayPal for reference #{reference}"
{ success: true }
end
end
module BankTransfer
def process_payment(amount)
puts "Initiating bank transfer of $#{amount} to #{account_number}"
{ success: true, reference: "BT-#{rand(1000..9999)}" }
end
def refund(reference, amount)
puts "Initiating bank refund of $#{amount} for reference #{reference}"
{ success: true }
end
end
end
class PaymentProcessor
include PaymentStrategies::CreditCard
# or PayPal, or BankTransfer depending on configuration
attr_reader :card_number
def initialize(card_number:)
@card_number = card_number
end
end
processor = PaymentProcessor.new(card_number: "4111111111111111")
result = processor.process_payment(99.99)
# => "Charging $99.99 to credit card 1111"
# => {:success=>true, :reference=>"CC-4523"}

Explanation: Each payment strategy is a module with process_payment and refund methods. The PaymentProcessor class includes the appropriate strategy module. This makes it easy to add new payment methods by creating new modules, following the Open/Closed Principle.


Example 3: Observer Pattern with Callbacks

Section titled “Example 3: Observer Pattern with Callbacks”

Problem: Implement an observer system where models can subscribe to changes on other models and react accordingly.

module Observable
def self.included(base)
base.instance_variable_set(:@observers, [])
base.extend(ClassMethods)
end
module ClassMethods
def observers
@observers ||= []
end
def add_observer(klass)
observers << klass unless observers.include?(klass)
end
end
def notify_observers(event, **data)
self.class.observers.each do |observer|
observer.new.handle_event(event, **data)
end
end
end
class EmailObserver
def handle_event(event, **data)
case event
when :user_created
UserMailer.welcome(data[:user]).deliver_later
puts "Welcome email queued for #{data[:user].name}"
when :order_placed
OrderMailer.confirmation(data[:order]).deliver_later
puts "Order confirmation queued for #{data[:order].id}"
end
end
end
class AuditObserver
def handle_event(event, **data)
AuditLog.create!(
event: event,
details: data.transform_values(&:to_s),
timestamp: Time.current
)
puts "Audit log created: #{event}"
end
end
class User < ApplicationRecord
include Observable
add_observer EmailObserver
add_observer AuditObserver
def after_create_callback
notify_observers(:user_created, user: self)
end
end

Explanation: The Observable module uses self.included to set up the observers array when included. add_observer registers observer classes. notify_observers iterates through observers and calls handle_event. Each observer type (Email, Audit) handles events differently, decoupling the notification logic from the model.

  • Methods and Blocks - How mixins via modules extend object behavior without inheritance
  • Metaprogramming - How method_missing and included hooks enable dynamic class behavior
  • Concurrency - How Ruby’s GIL affects object-oriented concurrent programming

Confusing == with equal? and eql?. == checks value equality (can be overridden). equal? checks object identity (same object in memory). eql? checks value and type equality (used as hash key). Students often assume == checks identity, which leads to unexpected behaviour with custom classes.

Forgetting that instance variables are private by default. Ruby instance variables cannot be accessed directly from outside the object. You must define accessor methods (attr_reader, attr_writer, attr_accessor). Students sometimes try to access @variable from outside the class, causing NoMethodError.

Not using modules for mixins instead of multiple inheritance. Ruby does not support multiple inheritance of classes, but modules provide a way to share behaviour. Students often create deep class hierarchies when a module mixin would be simpler and more flexible.