Ruby Object-Oriented Programming
Object-Oriented Programming (OOP) is one of Ruby's core features. In Ruby, everything is an object, making OOP very natural and powerful. Ruby provides rich OOP features including classes, objects, inheritance, modules, encapsulation, and polymorphism. This chapter will provide a detailed introduction to various concepts and best practices of OOP in Ruby.
🎯 Object-Oriented Basics
What is Object-Oriented Programming
Object-Oriented Programming is a programming paradigm that uses "objects" to design software. Objects are combinations of data and methods for operating on that data. OOP features in Ruby include:
- Class: Template or blueprint for objects
- Object: Instance of a class
- Encapsulation: Hide internal implementation details
- Inheritance: Create new classes from existing classes
- Polymorphism: Different implementations of the same interface
- Module: Code reuse and namespace
ruby
# Basic class definition
class Person
# Constructor
def initialize(name, age)
@name = name # Instance variable
@age = age
end
# Instance method
def introduce
"I am #{@name}, #{@age} years old"
end
# Getter method
def name
@name
end
# Setter method
def name=(new_name)
@name = new_name
end
end
# Create object
person = Person.new("Alice", 25)
puts person.introduce # I am Alice, 25 years old
# Use getter and setter
puts person.name # Alice
person.name = "Bob"
puts person.name # BobClasses and Objects Basics
ruby
# Class definition
class Car
# Class variable
@@total_cars = 0
# Constructor
def initialize(brand, model)
@brand = brand
@model = model
@mileage = 0
@@total_cars += 1
end
# Instance method
def drive(distance)
@mileage += distance
"Drove #{distance} km, total mileage #{@mileage} km"
end
# Class method
def self.total_cars
@@total_cars
end
# Instance variable accessors
attr_reader :brand, :model, :mileage
attr_writer :mileage
attr_accessor :color # Automatically generate getter and setter
end
# Create objects
car1 = Car.new("Toyota", "Corolla")
car2 = Car.new("Honda", "Accord")
# Use objects
puts car1.brand # Toyota
puts car1.drive(100) # Drove 100 km, total mileage 100 km
car1.color = "red"
puts car1.color # red
# Call class method
puts Car.total_cars # 2🏗️ Class Definition and Methods
Constructor and Initialization
ruby
class Student
# Use attr_accessor to automatically generate accessors
attr_accessor :name, :age, :grade
# Constructor
def initialize(name, age, grade = "Unknown")
@name = name
@age = age
@grade = grade
@courses = [] # Default value
end
# Method with default parameters
def enroll(course, semester = "Fall")
@courses << { name: course, semester: semester }
end
# Method with variable parameters
def add_grades(*grades)
@grades = grades.flatten
end
# Method with keyword parameters
def update_info(name: nil, age: nil, grade: nil)
@name = name if name
@age = age if age
@grade = grade if grade
end
# Method with block parameter
def with_logging
puts "Starting operation"
result = yield if block_given?
puts "Operation completed"
result
end
# Instance method
def info
"Name: #{@name}, Age: #{@age}, Grade: #{@grade}"
end
def courses
@courses.map { |course| "#{course[:name]} (#{course[:semester]})" }
end
end
# Using Student class
student = Student.new("Alice", 20, "Sophomore")
student.enroll("Math")
student.enroll("English", "Spring")
student.add_grades(85, 92, 78)
puts student.info
puts student.courses.inspect
student.update_info(age: 21, grade: "Junior")
puts student.info
student.with_logging { puts "Updating student information" }Access Control
ruby
class BankAccount
# Public methods
def initialize(account_number, initial_balance = 0)
@account_number = account_number
@balance = initial_balance
@transaction_history = []
end
# Public methods - anyone can call
def account_number
@account_number
end
def balance
@balance
end
def deposit(amount)
return false if amount <= 0
@balance += amount
log_transaction("Deposit", amount)
true
end
def withdraw(amount)
return false if amount <= 0 || amount > @balance
@balance -= amount
log_transaction("Withdrawal", -amount)
true
end
# Protected methods - only class and subclasses can call
protected
def log_transaction(type, amount)
transaction = {
type: type,
amount: amount,
balance: @balance,
timestamp: Time.now
}
@transaction_history << transaction
end
# Private methods - only class internals can call
private
def validate_amount(amount)
amount > 0 && amount.is_a?(Numeric)
end
def generate_statement
"Account: #{@account_number}, Balance: #{@balance}"
end
# Private methods can also be called by public methods
public
def print_statement
generate_statement # Can call private method
end
end
# Using bank account
account = BankAccount.new("123456789", 1000)
puts account.account_number # 123456789
puts account.balance # 1000
account.deposit(500)
puts account.balance # 1500
account.withdraw(200)
puts account.balance # 1300
puts account.print_statement # Account: 123456789, Balance: 1300
# account.log_transaction("Test", 100) # Error: protected method
# account.generate_statement # Error: private method🔗 Inheritance and Polymorphism
Class Inheritance
ruby
# Base class
class Animal
attr_accessor :name, :age
def initialize(name, age)
@name = name
@age = age
end
# Virtual method (subclass should override)
def speak
raise NotImplementedError, "Subclass must implement speak method"
end
# Public method
def info
"#{@name} is a #{@age}-year-old animal"
end
# Private method
private
def species
"Animal"
end
end
# Inherit base class
class Dog < Animal
attr_accessor :breed
def initialize(name, age, breed)
super(name, age) # Call parent class constructor
@breed = breed
end
# Override parent method
def speak
"#{@name} barks"
end
# Add new method
def fetch
"#{@name} fetches the ball"
end
# Override parent method and call parent method
def info
super + ", breed is #{@breed}"
end
end
class Cat < Animal
attr_accessor :color
def initialize(name, age, color)
super(name, age)
@color = color
end
# Override parent method
def speak
"#{@name} meows"
end
# Add new method
def climb
"#{@name} climbs a tree"
end
end
# Using inheritance
dog = Dog.new("Buddy", 3, "Golden Retriever")
cat = Cat.new("Whiskers", 2, "Orange")
puts dog.info # Buddy is a 3-year-old animal, breed is Golden Retriever
puts dog.speak # Buddy barks
puts dog.fetch # Buddy fetches the ball
puts cat.info # Whiskers is a 2-year-old animal
puts cat.speak # Whiskers meows
puts cat.climb # Whiskers climbs a treeMethod Lookup and super Keyword
ruby
class A
def method1
"A#method1"
end
def method2
"A#method2"
end
end
class B < A
def method1
"B#method1 (#{super})" # Call parent method
end
def method2
super # Directly call parent method
end
def method3
"B#method3"
end
end
class C < B
def method1
"C#method1 (#{super})" # Call B#method1, which calls A#method1
end
end
# Method lookup chain
a = A.new
b = B.new
c = C.new
puts a.method1 # A#method1
puts b.method1 # B#method1 (A#method1)
puts c.method1 # C#method1 (B#method1 (A#method1))
puts a.method2 # A#method2
puts b.method2 # A#method2
puts c.method2 # A#method2
puts b.method3 # B#method3
puts c.method3 # B#method3📦 Modules and Mixins
Module Definition and Usage
ruby
# Define module
module Drawable
def draw
"Drawing shape"
end
def erase
"Erasing shape"
end
# Module constant
PI = 3.14159
end
module Movable
def move(x, y)
"Moving to position (#{x}, #{y})"
end
def rotate(angle)
"Rotating #{angle} degrees"
end
end
# Using modules (include)
class Shape
include Drawable
include Movable
attr_accessor :x, :y
def initialize(x = 0, y = 0)
@x, @y = x, y
end
def position
"Position: (#{@x}, #{@y})"
end
end
# Using extended module (extend)
class Circle < Shape
extend Drawable # As class method extension
attr_accessor :radius
def initialize(x, y, radius)
super(x, y)
@radius = radius
end
def area
Math::PI * @radius ** 2
end
# Class method (obtained through extend)
def self.draw_circle
"Drawing circle"
end
end
# Using modules
shape = Shape.new(10, 20)
puts shape.draw # Drawing shape
puts shape.move(30, 40) # Moving to position (30, 40)
puts shape.position # Position: (30, 40)
circle = Circle.new(0, 0, 5)
puts circle.area # 78.53981633974483
puts circle.position # Position: (0, 0)
# Call class method
puts Circle.draw # Drawing shape (from Drawable module)
puts Circle.draw_circle # Drawing circleModule Mixins and Namespaces
ruby
# Namespace module
module Graphics
class Point
attr_accessor :x, :y
def initialize(x, y)
@x, @y = x, y
end
def distance(other)
Math.sqrt((@x - other.x) ** 2 + (@y - other.y) ** 2)
end
end
class Line
attr_accessor :start_point, :end_point
def initialize(start_point, end_point)
@start_point = start_point
@end_point = end_point
end
def length
@start_point.distance(@end_point)
end
end
end
# Using namespace
point1 = Graphics::Point.new(0, 0)
point2 = Graphics::Point.new(3, 4)
line = Graphics::Line.new(point1, point2)
puts point1.distance(point2) # 5.0
puts line.length # 5.0
# Mixin module providing shared functionality
module ComparableByAge
def <=>(other)
@age <=> other.age
end
def >(other)
(@age > other.age)
end
def <(other)
(@age < other.age)
end
def ==(other)
@age == other.age
end
end
class Person
include Comparable
include ComparableByAge
attr_accessor :name, :age
def initialize(name, age)
@name, @age = name, age
end
def to_s
"#{@name}(#{@age} years old)"
end
end
# Using comparison functionality
people = [
Person.new("Alice", 25),
Person.new("Bob", 30),
Person.new("Charlie", 20)
]
puts people.sort.map(&:to_s) # Charlie(20 years old), Alice(25 years old), Bob(30 years old)
puts people.max.to_s # Bob(30 years old)
puts people.min.to_s # Charlie(20 years old)🎯 Object-Oriented Practice Examples
User Management System
ruby
# Base user class
class User
attr_accessor :username, :email, :created_at
attr_reader :id
@@next_id = 1
@@users = {}
def initialize(username, email)
@id = @@next_id
@@next_id += 1
@username = username
@email = email
@created_at = Time.now
@active = true
@@users[@id] = self
end
def activate
@active = true
end
def deactivate
@active = false
end
def active?
@active
end
def info
"User ID: #{@id}, Username: #{@username}, Email: #{@email}, Status: #{@active ? 'active' : 'inactive'}"
end
# Class methods
def self.find(id)
@@users[id]
end
def self.all
@@users.values
end
def self.active_users
@@users.values.select(&:active?)
end
def self.deactivated_users
@@users.values.reject(&:active?)
end
end
# Admin user class
class AdminUser < User
attr_accessor :permissions
def initialize(username, email, permissions = [])
super(username, email)
@permissions = permissions
end
def grant_permission(permission)
@permissions << permission unless @permissions.include?(permission)
end
def revoke_permission(permission)
@permissions.delete(permission)
end
def can?(permission)
@permissions.include?(permission)
end
# Override info method
def info
super + ", Permissions: #{@permissions.join(', ')}"
end
# Admin-specific methods
def deactivate_user(user_id)
user = User.find(user_id)
user&.deactivate
end
def list_all_users
User.all.map(&:info)
end
end
# Using user management system
# Create regular users
user1 = User.new("zhangsan", "zhangsan@example.com")
user2 = User.new("lisi", "lisi@example.com")
# Create admin user
admin = AdminUser.new("admin", "admin@example.com", ["manage_users", "view_reports"])
# Admin operations
admin.grant_permission("delete_users")
puts admin.info
# Deactivate user
admin.deactivate_user(user2.id)
puts user2.active? # false
# List all users
puts "All users:"
User.all.each { |user| puts user.info }
puts "Active users count: #{User.active_users.length}"
puts "Inactive users count: #{User.deactivated_users.length}"Banking System
ruby
# Bank account base class
class BankAccount
attr_reader :account_number, :balance, :owner
@@next_account_number = 100000
@@accounts = {}
def initialize(owner, initial_balance = 0)
@account_number = @@next_account_number
@@next_account_number += 1
@owner = owner
@balance = initial_balance
@transactions = []
@@accounts[@account_number] = self
log_transaction("Account opening", initial_balance)
end
def deposit(amount)
return false if amount <= 0
@balance += amount
log_transaction("Deposit", amount)
true
end
def withdraw(amount)
return false if amount <= 0 || amount > @balance
@balance -= amount
log_transaction("Withdrawal", -amount)
true
end
def transfer_to(other_account, amount)
return false if amount <= 0 || amount > @balance
return false unless other_account.is_a?(BankAccount)
withdraw(amount)
other_account.deposit(amount)
log_transaction("Transfer to #{other_account.account_number}", -amount)
other_account.log_transaction("Transfer from #{@account_number}", amount)
true
end
def transaction_history
@transactions.dup
end
def self.find(account_number)
@@accounts[account_number]
end
def self.all_accounts
@@accounts.values
end
def self.total_balance
@@accounts.values.sum(&:balance)
end
protected
def log_transaction(type, amount)
transaction = {
type: type,
amount: amount,
balance: @balance,
timestamp: Time.now
}
@transactions << transaction
end
end
# Savings account
class SavingsAccount < BankAccount
def initialize(owner, initial_balance = 0, interest_rate = 0.02)
super(owner, initial_balance)
@interest_rate = interest_rate
@last_interest_date = Date.today
end
def add_interest
today = Date.today
return if today <= @last_interest_date
interest = @balance * @interest_rate
deposit(interest)
log_transaction("Interest", interest)
@last_interest_date = today
end
def info
"Savings Account #{@account_number}, Balance: #{@balance}, Interest Rate: #{@interest_rate * 100}%"
end
end
# Checking account
class CheckingAccount < BankAccount
def initialize(owner, initial_balance = 0, overdraft_limit = 0)
super(owner, initial_balance)
@overdraft_limit = overdraft_limit
end
# Override withdrawal method to support overdraft
def withdraw(amount)
return false if amount <= 0
return false if (@balance + @overdraft_limit) < amount
@balance -= amount
log_transaction("Withdrawal", -amount)
true
end
def info
"Checking Account #{@account_number}, Balance: #{@balance}, Overdraft Limit: #{@overdraft_limit}"
end
end
# Bank management system
class Bank
def initialize(name)
@name = name
@accounts = []
end
def open_savings_account(owner, initial_balance = 0, interest_rate = 0.02)
account = SavingsAccount.new(owner, initial_balance, interest_rate)
@accounts << account
account
end
def open_checking_account(owner, initial_balance = 0, overdraft_limit = 0)
account = CheckingAccount.new(owner, initial_balance, overdraft_limit)
@accounts << account
account
end
def find_account(account_number)
@accounts.find { |account| account.account_number == account_number }
end
def total_assets
@accounts.sum(&:balance)
end
def accounts_info
@accounts.map(&:info)
end
end
# Using banking system
bank = Bank.new("National Bank")
# Open accounts
savings = bank.open_savings_account("Alice", 10000, 0.03)
checking = bank.open_checking_account("Bob", 5000, 1000)
# Operate accounts
savings.deposit(2000)
checking.withdraw(6000) # Use overdraft limit
# Transfer
savings.transfer_to(checking, 3000)
# View account information
puts savings.info
puts checking.info
puts "Bank total assets: #{bank.total_assets}"
# View transaction history
puts "Savings account transaction history:"
savings.transaction_history.each do |transaction|
puts " #{transaction[:timestamp].strftime('%Y-%m-%d %H:%M')} - #{transaction[:type]}: #{transaction[:amount]}, Balance: #{transaction[:balance]}"
end📊 Object-Oriented Design Principles
SOLID Principles Application
ruby
# S - Single Responsibility Principle (SRP)
# Each class should have only one reason to change
# Bad design
class User
def initialize(name, email)
@name = name
@email = email
end
# User data management
def save
# Save to database
end
def validate
# Validate user data
end
# Email sending
def send_welcome_email
# Send welcome email
end
# Logging
def log_activity(activity)
# Log user activity
end
end
# Good design
class User
attr_accessor :name, :email
def initialize(name, email)
@name = name
@email = email
end
end
class UserDatabase
def self.save(user)
# Save user to database
end
def self.find(id)
# Find user from database
end
end
class UserValidator
def self.validate(user)
# Validate user data
end
end
class EmailService
def self.send_welcome_email(user)
# Send welcome email
end
end
class ActivityLogger
def self.log(user, activity)
# Log user activity
end
end
# O - Open/Closed Principle (OCP)
# Open for extension, closed for modification
# Base report class
class Report
def generate
data = fetch_data
format_data(data)
end
private
def fetch_data
# Common logic for fetching data
[]
end
def format_data(data)
# Default formatting
data.join("\n")
end
end
# Extend report class without modifying original code
class PDFReport < Report
def format_data(data)
# PDF formatting
"PDF: #{data.join(', ')}"
end
end
class ExcelReport < Report
def format_data(data)
# Excel formatting
"Excel: #{data.join("\t")}"
end
end
# L - Liskov Substitution Principle (LSP)
# Subclasses should be substitutable for their parent classes
class Bird
def fly
"Bird is flying"
end
end
class Sparrow < Bird
def fly
"Sparrow is flying"
end
end
class Ostrich < Bird
# Ostriches can't fly, violates Liskov Substitution Principle
def fly
raise "Ostrich can't fly"
end
end
# Improved design
class Bird
# Bird's basic functionality
end
class FlyingBird < Bird
def fly
"Bird is flying"
end
end
class Sparrow < FlyingBird
def fly
"Sparrow is flying"
end
end
class Ostrich < Bird
def run
"Ostrich is running"
end
end
# I - Interface Segregation Principle (ISP)
# Clients should not depend on interfaces they don't use
# Bad design
module Worker
def work
# Work
end
def eat
# Eat
end
def sleep
# Sleep
end
end
class HumanWorker
include Worker
def work
"Human is working"
end
def eat
"Human is eating"
end
def sleep
"Human is sleeping"
end
end
class RobotWorker
include Worker
def work
"Robot is working"
end
def eat
# Robots don't need to eat
raise "Robots don't eat"
end
def sleep
# Robots don't need to sleep
raise "Robots don't sleep"
end
end
# Good design
module Workable
def work
# Work
end
end
module Eatable
def eat
# Eat
end
end
module Sleepable
def sleep
# Sleep
end
end
class HumanWorker
include Workable
include Eatable
include Sleepable
def work
"Human is working"
end
def eat
"Human is eating"
end
def sleep
"Human is sleeping"
end
end
class RobotWorker
include Workable
def work
"Robot is working"
end
end
# D - Dependency Inversion Principle (DIP)
# Depend on abstractions, not concrete implementations
# Bad design
class EmailNotifier
def notify(message)
# Send email notification
puts "Sending email: #{message}"
end
end
class UserService
def initialize
@notifier = EmailNotifier.new
end
def create_user(user_data)
# User creation logic
@notifier.notify("User created")
end
end
# Good design
class Notifier
def notify(message)
raise NotImplementedError
end
end
class EmailNotifier < Notifier
def notify(message)
puts "Sending email: #{message}"
end
end
class SMSNotifier < Notifier
def notify(message)
puts "Sending SMS: #{message}"
end
end
class UserService
def initialize(notifier)
@notifier = notifier
end
def create_user(user_data)
# User creation logic
@notifier.notify("User created")
end
end
# Usage
email_notifier = EmailNotifier.new
sms_notifier = SMSNotifier.new
user_service1 = UserService.new(email_notifier)
user_service2 = UserService.new(sms_notifier)🛡️ Object-Oriented Best Practices
1. Design Pattern Applications
ruby
# Singleton pattern
class Logger
@@instance = nil
private_class_method :new
def self.instance
@@instance ||= new
end
def log(message)
puts "[#{Time.now}] #{message}"
end
private
def initialize
# Private constructor
end
end
# Using singleton
logger1 = Logger.instance
logger2 = Logger.instance
puts logger1.equal?(logger2) # true
logger1.log("First log")
logger2.log("Second log")
# Factory pattern
class AnimalFactory
def self.create_animal(type, name)
case type.downcase
when 'dog'
Dog.new(name)
when 'cat'
Cat.new(name)
when 'bird'
Bird.new(name)
else
raise "Unknown animal type: #{type}"
end
end
end
# Using factory
dog = AnimalFactory.create_animal('dog', 'Buddy')
cat = AnimalFactory.create_animal('cat', 'Whiskers')
# Observer pattern
class Subject
def initialize
@observers = []
end
def add_observer(observer)
@observers << observer
end
def remove_observer(observer)
@observers.delete(observer)
end
def notify_observers
@observers.each { |observer| observer.update(self) }
end
end
class TemperatureSensor < Subject
attr_reader :temperature
def initialize
super
@temperature = 0
end
def temperature=(temp)
@temperature = temp
notify_observers if temp > 30
end
end
class TemperatureDisplay
def update(subject)
puts "Warning: Temperature too high! Current temperature: #{subject.temperature}°C"
end
end
class TemperatureLogger
def update(subject)
puts "Recording temperature: #{subject.temperature}°C at #{Time.now}"
end
end
# Using observer pattern
sensor = TemperatureSensor.new
display = TemperatureDisplay.new
logger = TemperatureLogger.new
sensor.add_observer(display)
sensor.add_observer(logger)
sensor.temperature = 25 # No warning
sensor.temperature = 35 # Triggers warning2. Code Organization and Structure
ruby
# Modular design
module Payment
class Base
def initialize(amount)
@amount = amount
end
def process
raise NotImplementedError, "Subclass must implement process method"
end
end
class CreditCard < Base
def initialize(amount, card_number, cvv)
super(amount)
@card_number = card_number
@cvv = cvv
end
def process
"Processing credit card payment: #{@amount} dollars"
end
end
class Alipay < Base
def initialize(amount, account)
super(amount)
@account = account
end
def process
"Processing Alipay payment: #{@amount} dollars"
end
end
class WeChatPay < Base
def initialize(amount, openid)
super(amount)
@openid = openid
end
def process
"Processing WeChat Pay payment: #{@amount} dollars"
end
end
end
# Using payment module
credit_card = Payment::CreditCard.new(100, "1234****5678", "123")
alipay = Payment::Alipay.new(100, "user@example.com")
wechat = Payment::WeChatPay.new(100, "openid123")
puts credit_card.process
puts alipay.process
puts wechat.process
# Strategy pattern
class PaymentProcessor
def initialize(payment_strategy)
@payment_strategy = payment_strategy
end
def process_payment(amount)
@payment_strategy.process(amount)
end
end
# Using strategy pattern
processor1 = PaymentProcessor.new(Payment::CreditCard.new(100, "1234****5678", "123"))
processor2 = PaymentProcessor.new(Payment::Alipay.new(100, "user@example.com"))
puts processor1.process_payment(100)
puts processor2.process_payment(100)3. Test-Friendly Design
ruby
# Dependency injection improves testability
class OrderService
def initialize(payment_gateway = nil, inventory_service = nil)
@payment_gateway = payment_gateway || PaymentGateway.new
@inventory_service = inventory_service || InventoryService.new
end
def process_order(order)
# Check inventory
return false unless @inventory_service.check_stock(order.items)
# Process payment
payment_result = @payment_gateway.charge(order.total_amount)
return false unless payment_result.success?
# Update inventory
@inventory_service.update_stock(order.items)
true
end
end
# Mock objects for testing
class MockPaymentGateway
def charge(amount)
OpenStruct.new(success?: true)
end
end
class MockInventoryService
def check_stock(items)
true
end
def update_stock(items)
# Mock inventory update
end
end
# Test code
# order_service = OrderService.new(MockPaymentGateway.new, MockInventoryService.new)
# result = order_service.process_order(order)
# assert(result, "Order processing should succeed")📚 Next Steps
After mastering Ruby object-oriented programming, continue learning:
- Ruby Database Access - Learn database operations
- Ruby Network Programming - Learn Socket programming
- Ruby Multithreading - Master concurrent programming
- Ruby Web Services - Learn web service development
Continue your Ruby learning journey!