Object-Oriented Programming
Intuition
Section titled “Intuition”Object-oriented programming organises code around objects that bundle data with behaviour. Ruby implements OOP through classes, but everything is an object including classes themselves, enabling powerful reflection. Inheritance builds specialised types from general ones, and mixins through modules provide code reuse without the diamond problem of multiple inheritance. Encapsulation hides implementation details behind public interfaces, and polymorphism lets different objects respond to the same message in their own way.
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)Cross-References
Section titled “Cross-References”- Variables and Types defines the instance variables and data types used within object-oriented class definitions.
- Methods and Blocks covers the method definitions and block passing that are central to Ruby’s OOP style.
- Metaprogramming uses Ruby’s OOP features to dynamically define classes and methods at runtime.
Common Mistakes
Section titled “Common Mistakes”Confusing class variables (@@) with class instance variables: Class variables are shared across the entire inheritance hierarchy, leading to unexpected sharing. Use class instance variables (@ in class methods) for class-level state that shouldn’t be inherited.
Forgetting that self changes in blocks: Inside a block passed to class_eval or instance_eval, self refers to the evaluation context, not the original receiver. This can cause methods to be defined on the wrong object.
Using == instead of eql? and hash: By default, == compares object identity. Override both eql? and hash together for value-based equality in hashes and sets, or objects won’t behave correctly as keys.