Overview
Classes define blueprints for creating objects that combine data and behavior into cohesive units. An object represents a specific instance of a class, containing its own state while sharing structure and behavior defined by the class. This paradigm emerged from Simula in the 1960s and became widespread through Smalltalk, C++, Java, and Ruby.
The class-object relationship provides a template mechanism: classes specify what data objects hold and what operations they perform, while objects represent individual entities with distinct values. A Car class might define attributes like color and model, plus behaviors like start and stop. Each car object maintains its own color and model values while sharing the start and stop implementations.
Ruby treats everything as an object, including classes themselves. Classes are first-class objects, instances of the Class class. This design allows runtime class modification, dynamic method definition, and metaprogramming capabilities that distinguish Ruby from statically-typed languages.
# Class definition
class Book
def initialize(title, author)
@title = title
@author = author
end
def description
"#{@title} by #{@author}"
end
end
# Object creation
book = Book.new("1984", "George Orwell")
book.description
# => "1984 by George Orwell"
Classes organize related functionality, reduce code duplication through inheritance, and provide interfaces that hide implementation details. Objects maintain state across method calls, enabling stateful programming where data persists between operations.
Key Principles
Encapsulation bundles data and methods operating on that data within a single unit, restricting direct access to internal state. Objects expose public interfaces while hiding implementation details. Ruby implements encapsulation through access control keywords: public, private, and protected. Instance variables remain private by default, accessible only through defined methods.
class BankAccount
def initialize(balance)
@balance = balance
end
def deposit(amount)
@balance += amount if amount > 0
end
def balance
@balance
end
private
def validate_withdrawal(amount)
amount > 0 && amount <= @balance
end
end
The @balance instance variable cannot be accessed directly from outside the class. The validate_withdrawal method remains internal to the class implementation, preventing external code from calling it.
Abstraction exposes essential features while hiding unnecessary complexity. Classes define interfaces representing object capabilities without revealing implementation details. Abstract concepts translate to concrete code through class design. A EmailService class might expose a send method without revealing SMTP protocol details, connection pooling, or retry logic.
Identity and State distinguish objects. Identity means each object occupies distinct memory space, even when containing identical data. State comprises the values stored in an object's instance variables at any moment. Objects modify their state through method calls, maintaining data across operations.
account1 = BankAccount.new(1000)
account2 = BankAccount.new(1000)
account1.object_id == account2.object_id
# => false (different objects)
account1.deposit(500)
# account1 state changed, account2 unchanged
Behavior and Methods define operations objects perform. Methods access and modify object state, implement business logic, and interact with other objects. Instance methods operate on specific object instances, accessing instance variables unique to each object. Class methods operate on the class itself, independent of individual instances.
Message Passing describes how objects interact. Calling a method on an object sends a message to that object. The object determines how to handle the message through method lookup. Ruby's dynamic nature allows objects to respond to messages not explicitly defined through method_missing.
class Logger
def method_missing(method_name, *args)
puts "[#{method_name.upcase}] #{args.join(' ')}"
end
end
logger = Logger.new
logger.error("Connection failed")
# => "[ERROR] Connection failed"
Type and Class Membership establish object categories. An object's class determines its type, defining what messages it responds to and what behavior it exhibits. Ruby uses duck typing: if an object responds to required methods, its class matters less than its capabilities. This differs from nominal typing where class hierarchy determines type compatibility.
Class Hierarchy and Inheritance enable code reuse through parent-child relationships. Subclasses inherit methods and behavior from parent classes, overriding or extending functionality. Ruby supports single inheritance where each class has one direct parent, but includes modules for mixin-based multiple inheritance.
Ruby Implementation
Ruby implements classes using the class keyword, defining a namespace for methods and constants. Class names start with capital letters following constant naming conventions. The initialize method serves as the constructor, called automatically when creating new objects through new.
class Product
attr_reader :name, :price
attr_accessor :quantity
def initialize(name, price, quantity = 0)
@name = name
@price = price
@quantity = quantity
end
def total_value
@price * @quantity
end
def restock(amount)
@quantity += amount
end
end
product = Product.new("Widget", 25.50, 100)
product.total_value
# => 2550.0
Instance Variables store object state, denoted by the @ prefix. Each object maintains separate instance variables, even for objects of the same class. Instance variables don't require declaration and initialize when first assigned. Accessing uninitialized instance variables returns nil without raising errors.
Attribute Accessors provide shorthand for defining getter and setter methods. The attr_reader creates getter methods, attr_writer creates setter methods, and attr_accessor creates both. These macros generate methods at class definition time, eliminating boilerplate code.
class Person
attr_accessor :name
attr_reader :birth_year
def initialize(name, birth_year)
@name = name
@birth_year = birth_year
end
def age
Time.now.year - @birth_year
end
end
Class Variables and Methods provide shared state and behavior across all instances. Class variables use @@ prefix and maintain single values shared by all objects. Class methods, defined with self.method_name or within class << self blocks, operate on the class rather than instances.
class Counter
@@count = 0
def initialize
@@count += 1
end
def self.total_count
@@count
end
end
Counter.new
Counter.new
Counter.total_count
# => 2
Class variables present challenges in inheritance hierarchies, as subclasses share the same variable with parent classes. Class instance variables, stored as instance variables of the class object itself, provide alternatives for class-level state without sharing concerns.
class Tracker
@items = []
class << self
attr_accessor :items
end
def self.add(item)
@items << item
end
end
Inheritance extends class functionality through the < operator. Subclasses inherit all methods from parent classes, adding or overriding behavior. The super keyword calls the parent class method with the same name, enabling method extension rather than complete replacement.
class Vehicle
def initialize(make, model)
@make = make
@model = model
end
def description
"#{@make} #{@model}"
end
end
class Car < Vehicle
def initialize(make, model, doors)
super(make, model)
@doors = doors
end
def description
"#{super} with #{@doors} doors"
end
end
car = Car.new("Toyota", "Camry", 4)
car.description
# => "Toyota Camry with 4 doors"
Modules and Mixins provide multiple inheritance capabilities through composition. Modules define reusable functionality included in classes through include (for instance methods) or extend (for class methods). Modules appear in the method lookup chain between a class and its parent.
module Loggable
def log(message)
puts "[#{self.class}] #{message}"
end
end
class Service
include Loggable
def perform
log("Starting service")
# service logic
log("Service completed")
end
end
Access Control restricts method visibility. Public methods form the class interface, callable from anywhere. Private methods remain callable only within the class, not even with explicit receivers. Protected methods allow calls from instances of the same class or subclasses.
class Account
def initialize(balance)
@balance = balance
end
def transfer(amount, target)
return false unless sufficient_funds?(amount)
withdraw(amount)
target.deposit(amount)
true
end
protected
def deposit(amount)
@balance += amount
end
private
def withdraw(amount)
@balance -= amount
end
def sufficient_funds?(amount)
@balance >= amount
end
end
Singleton Methods define behavior specific to individual objects rather than all instances. These methods exist only on particular objects, created through def object.method_name syntax or using define_singleton_method.
str = "hello"
def str.shout
upcase + "!!!"
end
str.shout
# => "HELLO!!!"
"hello".shout
# => NoMethodError
Method Visibility and Refinements control method scope and modifications. Refinements limit monkey patching scope through lexically-scoped modifications, preventing global class modifications from affecting unrelated code.
Practical Examples
Domain Modeling with Classes translates real-world concepts into code structures. Consider an e-commerce order system requiring products, orders, and customers. Classes model each entity with appropriate state and behavior.
class Customer
attr_reader :id, :email, :orders
def initialize(id, email)
@id = id
@email = email
@orders = []
end
def place_order(order)
@orders << order
order.customer = self
end
def total_spent
@orders.sum(&:total)
end
end
class Order
attr_accessor :customer
attr_reader :id, :items, :created_at
def initialize(id)
@id = id
@items = []
@created_at = Time.now
end
def add_item(product, quantity)
@items << OrderItem.new(product, quantity)
end
def total
@items.sum(&:subtotal)
end
def item_count
@items.sum(&:quantity)
end
end
class OrderItem
attr_reader :product, :quantity
def initialize(product, quantity)
@product = product
@quantity = quantity
end
def subtotal
@product.price * @quantity
end
end
# Usage
customer = Customer.new(1, "user@example.com")
order = Order.new(101)
order.add_item(Product.new("Widget", 10.00), 3)
order.add_item(Product.new("Gadget", 25.00), 2)
customer.place_order(order)
customer.total_spent
# => 80.0
This design separates concerns: customers manage order history, orders coordinate items, and order items calculate subtotals. Each class maintains focused responsibility.
State Machines with Objects model entities transitioning between defined states. A document workflow system demonstrates state-dependent behavior where valid operations depend on current state.
class Document
attr_reader :state, :content
STATES = [:draft, :review, :approved, :published, :archived]
def initialize(content)
@content = content
@state = :draft
@history = []
end
def submit_for_review
return false unless @state == :draft
transition_to(:review)
end
def approve
return false unless @state == :review
transition_to(:approved)
end
def publish
return false unless @state == :approved
transition_to(:published)
end
def archive
return false unless [:published, :approved].include?(@state)
transition_to(:archived)
end
def reject
return false unless @state == :review
transition_to(:draft)
end
private
def transition_to(new_state)
@history << { from: @state, to: new_state, at: Time.now }
@state = new_state
true
end
end
Objects maintain state and enforce valid transitions, preventing invalid operations. The state determines available operations, encapsulating workflow rules within the class.
Builder Pattern for Complex Construction addresses objects requiring numerous parameters or complex initialization. The builder provides fluent interface for step-by-step construction.
class QueryBuilder
def initialize(table)
@table = table
@conditions = []
@order = nil
@limit = nil
end
def where(condition)
@conditions << condition
self
end
def order_by(column, direction = :asc)
@order = { column: column, direction: direction }
self
end
def limit(count)
@limit = count
self
end
def to_sql
sql = "SELECT * FROM #{@table}"
sql += " WHERE #{@conditions.join(' AND ')}" unless @conditions.empty?
sql += " ORDER BY #{@order[:column]} #{@order[:direction].to_s.upcase}" if @order
sql += " LIMIT #{@limit}" if @limit
sql
end
end
query = QueryBuilder.new(:users)
.where("age >= 18")
.where("status = 'active'")
.order_by(:created_at, :desc)
.limit(10)
.to_sql
# => "SELECT * FROM users WHERE age >= 18 AND status = 'active' ORDER BY created_at DESC LIMIT 10"
Returning self from methods enables method chaining, constructing complex objects through readable sequential calls.
Repository Pattern for Data Access separates domain logic from persistence concerns. Repository classes encapsulate data access operations, providing clean interfaces for storing and retrieving objects.
class UserRepository
def initialize(database)
@database = database
end
def find(id)
data = @database.query("SELECT * FROM users WHERE id = ?", id).first
return nil unless data
build_user(data)
end
def find_by_email(email)
data = @database.query("SELECT * FROM users WHERE email = ?", email).first
return nil unless data
build_user(data)
end
def save(user)
if user.id
update(user)
else
insert(user)
end
end
def all
@database.query("SELECT * FROM users").map { |data| build_user(data) }
end
private
def build_user(data)
User.new(
id: data['id'],
email: data['email'],
name: data['name']
)
end
def insert(user)
result = @database.execute(
"INSERT INTO users (email, name) VALUES (?, ?)",
user.email, user.name
)
user.id = result.last_insert_id
user
end
def update(user)
@database.execute(
"UPDATE users SET email = ?, name = ? WHERE id = ?",
user.email, user.name, user.id
)
user
end
end
Repositories centralize data access logic, simplifying testing through mock database injection and enabling database technology changes without affecting business logic.
Design Considerations
When Classes Provide Value depends on problem characteristics. Classes excel at modeling entities with identity, state, and behavior. Systems requiring object lifecycle management, state transitions, or polymorphic behavior benefit from class-based design. Complex domains with multiple related concepts gain clarity through classes representing each concept.
Objects containing mutable state that changes over time suit class-based modeling. Shopping carts, user sessions, and game characters maintain changing state across operations. Classes encapsulate state modifications behind methods, preventing invalid state transitions.
Functional Alternatives exist when operations dominate data. Pure functions operating on immutable data structures avoid state management complexity. Problems decomposing into transformations on data pipelines often prefer functional approaches over object-oriented designs.
# Object-oriented approach
class Calculator
def initialize(value)
@value = value
end
def add(n)
@value += n
self
end
def multiply(n)
@value *= n
self
end
def result
@value
end
end
# Functional approach
def add(value, n)
value + n
end
def multiply(value, n)
value * n
end
result = multiply(add(10, 5), 2)
# => 30
Functional approaches eliminate state management but sacrifice object identity and encapsulation benefits. Choose based on whether identity and state management provide value for the problem.
Struct and OpenStruct Alternatives offer lightweight options for simple data containers. Structs define classes with attributes without writing full class definitions, suitable for value objects without complex behavior.
Customer = Struct.new(:name, :email, :age) do
def adult?
age >= 18
end
end
customer = Customer.new("Alice", "alice@example.com", 25)
customer.adult?
# => true
OpenStruct provides dynamic attribute assignment without predefining structure, useful for configuration objects or test doubles. OpenStruct trades performance and safety for flexibility.
Class Granularity Trade-offs balance between large monolithic classes and excessive fragmentation. Large classes violate single responsibility but avoid coordination complexity. Small focused classes provide flexibility at the cost of increased file count and navigation difficulty.
Extract classes when responsibilities cluster naturally or when classes exceed comprehension size. Methods operating on distinct subsets of instance variables suggest separate classes. Classes with multiple reasons to change indicate missing abstractions.
Inheritance Versus Composition represents fundamental design choices. Inheritance creates "is-a" relationships, composition creates "has-a" relationships. Inheritance couples subclasses to parent implementation details, making changes risky. Composition provides flexibility through object replacement.
# Inheritance approach
class Animal
def move
puts "Moving"
end
end
class Bird < Animal
def fly
puts "Flying"
end
end
# Composition approach
class Bird
def initialize
@movement = Flying.new
end
def move
@movement.execute
end
end
class Flying
def execute
puts "Flying"
end
end
Prefer composition over inheritance except when modeling genuine hierarchical relationships. Composition enables runtime behavior changes and avoids fragile base class problems.
Module Mixins Versus Inheritance provide different code reuse mechanisms. Modules offer multiple inheritance capabilities without commitment to class hierarchies. Include modules for shared behavior across unrelated classes.
module Timestampable
def touch
@updated_at = Time.now
end
def updated_at
@updated_at
end
end
class Post
include Timestampable
# Post-specific code
end
class Comment
include Timestampable
# Comment-specific code
end
Modules work well for horizontal concerns cutting across class hierarchies. Use inheritance for vertical specialization within conceptual hierarchies.
Common Patterns
Factory Pattern centralizes object creation logic, particularly when instantiation requires configuration or decision logic. Factories decouple client code from concrete classes, enabling polymorphism and testing flexibility.
class ReportFactory
def self.create(type, data)
case type
when :pdf
PdfReport.new(data)
when :csv
CsvReport.new(data)
when :html
HtmlReport.new(data)
else
raise ArgumentError, "Unknown report type: #{type}"
end
end
end
report = ReportFactory.create(:pdf, sales_data)
report.generate
Factories isolate creation complexity from usage, allowing creation logic changes without affecting consumers. Abstract factories extend this pattern for creating families of related objects.
Singleton Pattern restricts class instantiation to single shared instances. Ruby implements singletons through the Singleton module, ensuring only one instance exists throughout program execution.
require 'singleton'
class Configuration
include Singleton
attr_accessor :api_key, :endpoint
def initialize
@api_key = nil
@endpoint = "https://api.example.com"
end
end
config = Configuration.instance
config.api_key = "secret123"
# Same instance everywhere
Configuration.instance.api_key
# => "secret123"
Singletons suit configuration, logging, and connection pooling where multiple instances create problems. However, singletons complicate testing through global state and tight coupling.
Template Method Pattern defines algorithm skeletons in base classes while subclasses implement specific steps. Base classes control flow, subclasses customize behavior at defined extension points.
class DataImporter
def import(file)
data = read_file(file)
parsed = parse_data(data)
validated = validate_data(parsed)
save_data(validated)
end
def read_file(file)
File.read(file)
end
def validate_data(data)
data.select { |row| row_valid?(row) }
end
def row_valid?(row)
!row.empty?
end
def parse_data(data)
raise NotImplementedError, "Subclass must implement parse_data"
end
def save_data(data)
raise NotImplementedError, "Subclass must implement save_data"
end
end
class CsvImporter < DataImporter
def parse_data(data)
CSV.parse(data, headers: true)
end
def save_data(data)
data.each { |row| Database.insert(row) }
end
end
Template methods establish consistent processing flows while allowing customization. This pattern reduces code duplication across similar algorithms.
Observer Pattern establishes one-to-many dependencies where state changes in one object trigger notifications to dependent objects. Observers register interest in subject state changes, receiving updates automatically.
class Subject
def initialize
@observers = []
end
def attach(observer)
@observers << observer
end
def detach(observer)
@observers.delete(observer)
end
def notify(event)
@observers.each { |observer| observer.update(event) }
end
end
class StockPrice < Subject
attr_reader :symbol, :price
def initialize(symbol, price)
super()
@symbol = symbol
@price = price
end
def price=(new_price)
old_price = @price
@price = new_price
notify({ symbol: @symbol, old: old_price, new: new_price })
end
end
class PriceAlert
def initialize(threshold)
@threshold = threshold
end
def update(event)
if event[:new] > @threshold
puts "Alert: #{event[:symbol]} exceeded #{@threshold}"
end
end
end
stock = StockPrice.new("ACME", 100)
alert = PriceAlert.new(105)
stock.attach(alert)
stock.price = 110
# => "Alert: ACME exceeded 105"
Observers decouple subjects from dependent objects, enabling dynamic subscription management and flexible notification mechanisms.
Decorator Pattern adds responsibilities to objects dynamically without affecting other instances. Decorators wrap objects, forwarding requests while adding behavior before or after delegation.
class Text
attr_reader :content
def initialize(content)
@content = content
end
def render
@content
end
end
class BoldDecorator
def initialize(text)
@text = text
end
def render
"<b>#{@text.render}</b>"
end
end
class ItalicDecorator
def initialize(text)
@text = text
end
def render
"<i>#{@text.render}</i>"
end
end
text = Text.new("Hello")
bold_text = BoldDecorator.new(text)
italic_bold_text = ItalicDecorator.new(bold_text)
italic_bold_text.render
# => "<i><b>Hello</b></i>"
Decorators compose behavior at runtime, avoiding subclass explosion from combining features through inheritance. Each decorator focuses on single concerns.
Strategy Pattern encapsulates algorithms in separate classes, making them interchangeable. Contexts use strategies through common interfaces, enabling runtime algorithm selection.
class Context
attr_writer :strategy
def execute(data)
@strategy.process(data)
end
end
class QuickSort
def process(data)
# quicksort implementation
data.sort
end
end
class MergeSort
def process(data)
# mergesort implementation
data.sort
end
end
context = Context.new
context.strategy = QuickSort.new
context.execute([3, 1, 2])
# Switch strategy at runtime
context.strategy = MergeSort.new
context.execute([3, 1, 2])
Strategies eliminate conditional logic for algorithm selection, isolating algorithm variations in separate classes. This pattern supports open-closed principle: extending algorithms without modifying existing code.
Reference
Class Definition Syntax
| Syntax | Purpose | Example |
|---|---|---|
| class ClassName | Define new class | class User |
| class Child < Parent | Define subclass | class Admin < User |
| initialize | Constructor method | def initialize(name) |
| self | Reference current instance | self.update |
| super | Call parent method | super(args) |
| class << self | Open eigenclass | class << self; def method; end; end |
Instance Variables and Attributes
| Feature | Syntax | Description |
|---|---|---|
| Instance variable | @variable | Object-specific state |
| Class variable | @@variable | Shared across all instances |
| attr_reader | attr_reader :name | Generate getter method |
| attr_writer | attr_writer :name | Generate setter method |
| attr_accessor | attr_accessor :name | Generate getter and setter |
Access Control
| Modifier | Visibility | Callable From |
|---|---|---|
| public | Everywhere | Any code |
| protected | Within class hierarchy | Same class and subclasses |
| private | Within class only | Only without explicit receiver |
Common Class Methods
| Method | Purpose | Returns |
|---|---|---|
| new | Create instance | New object |
| allocate | Create uninitialized instance | New object |
| superclass | Get parent class | Class or nil |
| ancestors | Get inheritance chain | Array of modules and classes |
| instance_methods | List instance methods | Array of symbols |
| class_variables | List class variables | Array of symbols |
Object Methods
| Method | Purpose | Example |
|---|---|---|
| class | Get object's class | obj.class |
| instance_of? | Check exact class | obj.instance_of?(String) |
| is_a? or kind_of? | Check class hierarchy | obj.is_a?(Numeric) |
| respond_to? | Check method availability | obj.respond_to?(:to_s) |
| object_id | Get unique identifier | obj.object_id |
| freeze | Make immutable | obj.freeze |
| frozen? | Check if frozen | obj.frozen? |
Method Definition Options
| Pattern | Description | Usage |
|---|---|---|
| def method(arg) | Instance method | Regular methods |
| def self.method(arg) | Class method | Called on class itself |
| def method(arg = default) | Default parameter | Optional arguments |
| def method(*args) | Variable arguments | Accept any number of arguments |
| def method(arg:) | Keyword argument required | Named parameters |
| def method(arg: default) | Keyword argument optional | Named with defaults |
| def method(**kwargs) | Keyword argument collection | Capture all keyword args |
Inheritance Concepts
| Concept | Description | Consideration |
|---|---|---|
| Single inheritance | One parent class | Ruby supports only single |
| Method override | Replace parent method | Use same method name |
| Method extension | Enhance parent method | Call super then add behavior |
| Abstract methods | Unimplemented in parent | Raise NotImplementedError |
| Hook methods | Callbacks for events | inherited, included, extended |
Module Inclusion
| Method | Effect | Use Case |
|---|---|---|
| include | Add as ancestors | Instance methods |
| prepend | Add before class in chain | Override instance methods |
| extend | Add to eigenclass | Class methods or singleton methods |
Design Guidelines
| Principle | Description | Application |
|---|---|---|
| Single responsibility | One reason to change | Focused class purpose |
| Open-closed | Open for extension, closed for modification | Use inheritance and composition |
| Liskov substitution | Subclasses replaceable for parents | Maintain interface contracts |
| Interface segregation | Many specific interfaces over general | Prefer small focused modules |
| Dependency inversion | Depend on abstractions | Use duck typing and interfaces |
Common Method Patterns
| Pattern | Code Structure | Purpose |
|---|---|---|
| Query method | def status? | Return boolean |
| Command method | def update | Perform action |
| Factory method | def self.create | Object creation |
| Builder method | def with_option; self; end | Fluent interface |
| Template method | def process; step1; step2; end | Algorithm skeleton |
Class Relationships
| Relationship | Implementation | Example |
|---|---|---|
| Inheritance | class Child < Parent | User < Person |
| Composition | Has instance variable | Engine in Car |
| Aggregation | References other objects | Students in Course |
| Association | Knows about other class | Order references Customer |
| Dependency | Uses another class | Controller uses Service |