Object-Oriented Programming | Ruby
Classes
Section titled “Classes”Class Definition
Section titled “Class Definition”## Basic classclass 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}" endend
alice = Person.new("Alice", 30)puts alice.greet # => "Hello, I'm Alice, age 30"
## Classes are first-class objectsPerson.class # => ClassPerson.superclass # => ObjectPerson.ancestors # => [Person, Object, Kernel, BasicObject]
# Class names are constantsPerson = Class.new do def initialize(name) @name = name end
def to_s @name endendattr_accessor, attr_reader, attr_writer
Section titled “attr_accessor, attr_reader, attr_writer”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 endend
book = Book.new("Ruby Guide", "Matz", 39.99)book.title # => "Ruby Guide"book.price = 49.99 # calls the custom setterbook.isbn = "978-123" # generated setterinitialize and new
Section titled “initialize and new”class Point def initialize(x = 0, y = 0) @x = x @y = y endend
Point.new(3, 4) # creates a new Point instance
# initialize is a private methodPoint.instance_method(:initialize).name # => :initialize
# allocate creates an instance without calling initializeraw = Point.allocateraw.instance_variables # => [] (no @x, @y set)
# Overriding new for custom object creationclass 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 :newendInheritance
Section titled “Inheritance”# Base classclass 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})" endend
# Subclass with superclass 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}" endend
class Cat < Animal def speak "#{name} meows!" endend
rex = Dog.new("Rex", 5, "Labrador")puts rex.speak # => "Rex barks!"puts rex.info # => "Rex (Dog) -- Labrador"
# super behaviourclass Base def greet "Hello" endend
class Child < Base def greet super + " from Child" # passes args to parent super() # calls parent with no args endend
# Method resolution order (MRO)class A; endclass B < A; endclass C < A; endclass D < B; end
D.ancestors # => [D, B, A, Object, Kernel, BasicObject]Modules
Section titled “Modules”Modules serve two purposes: namespaces and mixins.
Modules as Namespaces
Section titled “Modules as Namespaces”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 endend
MathEngine.circle_area(5) # => 78.5398...MathEngine::PI # => 3.14159...v = MathEngine::Vector2D.new(3, 4)v.magnitude # => 5.0Modules as Mixins (include / extend / prepend)
Section titled “Modules as Mixins (include / extend / prepend)”# A module with instance methodsmodule Validation def validate! raise "Invalid state" unless valid? end
def valid? true endend
# include: adds instance methodsclass User include Validation
def initialize(name, email) @name = name @email = email end
def valid? !@name.nil? && !@email.nil? && @email.include?("@") endend
user = User.new("Alice", "alice@example.com")user.valid? # => trueuser.validate! # no error
# extend: adds methods as singleton methods (class-level on instance)class Config extend Validationend
Config.valid? # => trueConfig.validate! # no error
# prepend: adds methods before the class in the lookup chainmodule Logging def save puts "Before save: #{self.inspect}" super puts "After save: #{self.inspect}" endend
class Document prepend Logging
def save puts "Saving document" endend
doc = Document.newdoc.save# => "Before save: #<Document:...>"# => "Saving document"# => "After save: #<Document:...>"include vs extend vs prepend
Section titled “include vs extend vs prepend”module A def hello "A#hello" endend
module B def hello "B#hello" endend
class Example include A include Bend
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 Bend
Example2.ancestors# => [B, Example2, A, Object, Kernel, BasicObject]
Example2.new.hello # => "B#hello"
# Class-level extend vs includeclass Klass include M # instance methods from Mendclass Klass extend M # class methods from Mend
# module_function: methods become both instance and module methodsmodule 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) endend
Utilities.factorial(5) # => 120Utilities.fibonacci(10) # => 55
# Included in a classclass Calculator include Utilitiesend
calc = Calculator.newcalc.factorial(5) # => 120 (available as instance method)Class Methods (self.)
Section titled “Class Methods (self.)”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 endend
User.count # => 0alice = User.new("Alice")User.count # => 1
# Class methods are singleton methods on the class objectUser.singleton_methods # => [:count, :find_by_name, :all_users, :search, :reset!]Class Variables vs Instance Variables
Section titled “Class Variables vs Instance Variables”class Parent @@family = "shared"
def self.family @@family endend
class Child < Parent @@family = "overridden"end
Parent.family # => "overridden" -- class variables are shared across hierarchy!
# Safer alternative: class instance variablesclass SafeParent @family = "parent default"
class << self attr_accessor :family endend
class SafeChild < SafeParent @family = "child default"end
SafeParent.family # => "parent default"SafeChild.family # => "child default" -- not shared!
# Instance variables belong to a specific instanceclass Counter def initialize @count = 0 end
def increment @count += 1 end
def count @count endend
c1 = Counter.newc2 = Counter.newc1.incrementc1.incrementc2.incrementc1.count # => 2c2.count # => 1 (independent instances)Access Control
Section titled “Access Control”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 endend
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 endend
# 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)Access Control Keywords
Section titled “Access Control Keywords”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 publicend
# Per-method visibilityclass Example2 def method_a; end def method_b; end
private :method_b
def method_c; endendComparable Module
Section titled “Comparable Module”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 endend
v1 = Version.new(1, 2, 3)v2 = Version.new(1, 2, 10)v3 = Version.new(2, 0, 0)
v1 < v2 # => truev1 == v2 # => falsev2 < v3 # => truev1 <=> v2 # => -1v1.between?(v2, v3) # => true (from Comparable)[v2, v1, v3].sort.map(&:to_s) # => ["1.2.3", "1.2.10", "2.0.0"]Enumerable Module
Section titled “Enumerable Module”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 endend
wl = WordList.newwl.add("Ruby").add("Python").add("JavaScript").add("Ruby")
# All Enumerable methods availablewl.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 # => 4wl.uniq # => ["ruby", "python", "javascript"]wl.sort # => ["javascript", "python", "ruby"]wl.any? { |w| w.length > 10 } # => falsewl.all? { |w| w.length > 2 } # => truewl.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 supportclass 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 endend
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 # => 88Practical Patterns
Section titled “Practical Patterns”Composition over Inheritance
Section titled “Composition over Inheritance”class Engine def initialize(horsepower) @horsepower = horsepower end
def start puts "Engine started (#{@horsepower} HP)" end
def stop puts "Engine stopped" endend
class GPS def navigate(destination) puts "Navigating to #{destination}" endend
class Car def initialize(horsepower) @engine = Engine.new(horsepower) @gps = GPS.new end
def start @engine.start end
def navigate(destination) @gps.navigate(destination) endend
car = Car.new(200)car.startcar.navigate("London")Singleton Pattern
Section titled “Singleton Pattern”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 endend
db = DatabaseConnection.instancedb.connectdb.query("SELECT * FROM users")Observer Pattern
Section titled “Observer Pattern”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) } endend
class EventPublisher include Observable
def publish(event) puts "Publishing: #{event}" notify_observers(event) endend
class Logger def update(event) puts "[LOG] #{event}" endend
class Metrics def update(event) puts "[METRICS] event recorded" endend
publisher = EventPublisher.newpublisher.add_observer(Logger.new)publisher.add_observer(Metrics.new)publisher.publish("user_signed_up")Struct and OpenStruct
Section titled “Struct and OpenStruct”# Struct: lightweight class creationPerson = Struct.new(:name, :email, :age) do def adult? age >= 18 endend
p = Person.new("Alice", "a@b.com", 30)p.name # => "Alice"p.adult? # => truep[: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 objectrequire 'ostruct'
config = OpenStruct.new(host: "localhost", port: 8080)config.host # => "localhost"config.port # => 8080config.timeout = 30 # dynamically add fieldsconfig.timeout # => 30Data Class (Ruby 3.2+)
Section titled “Data Class (Ruby 3.2+)”# Data: immutable value objectsclass Point < Data params :x, :yend
p = Point.new(3, 4)p.x # => 3p.y # => 4p.frozen? # => truep == Point.new(3, 4) # => true (value equality)Method Lookup Chain
Section titled “Method Lookup Chain”module M1 def greet; puts "M1#greet"; endend
module M2 def greet; puts "M2#greet"; endend
class Base def greet; puts "Base#greet"; super; endend
class Child < Base include M1 include M2 def greet; puts "Child#greet"; super; endend
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]Intuition
Section titled “Intuition”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.
Worked Examples
Section titled “Worked Examples”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 endend
class User < ApplicationRecord include Authenticatable validates :email, presence: true, uniqueness: trueend
class Admin < ApplicationRecord include Authenticatable validates :role, inclusion: { in: %w[superadmin admin editor] }endExplanation: 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.
Example 2: Strategy Pattern with Modules
Section titled “Example 2: Strategy Pattern with Modules”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 endend
class PaymentProcessor include PaymentStrategies::CreditCard # or PayPal, or BankTransfer depending on configuration
attr_reader :card_number
def initialize(card_number:) @card_number = card_number endend
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 endend
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 endend
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}" endend
class User < ApplicationRecord include Observable add_observer EmailObserver add_observer AuditObserver
def after_create_callback notify_observers(:user_created, user: self) endendExplanation: 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.
Cross-References
Section titled “Cross-References”- 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
Common Mistakes
Section titled “Common Mistakes”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.