Classes and OOP
Ruby is a pure object-oriented language: every value is an instance of some class, every operation is a method call on a receiver, classes are themselves first-class objects (instances of Class). The class system is single-inheritance — a class extends at most one parent class — but admits multiple inheritance via mixins (modules included into a class). Classes are open — existing classes may be reopened and extended; the conventional contemporary discipline reserves “monkey patching” for genuinely necessary cases. The combination — pure object-orientation, classes as first-class objects, single inheritance plus mixins, dynamic message dispatch — is the substance of Ruby’s object model.
Class declarations
The class keyword introduces a class:
class Person
def initialize(name, age)
@name = name
@age = age
end
def greet
"Hello, #{@name}"
end
end
p = Person.new("Alice", 30)
puts p.greet # "Hello, Alice"
The form: class Name ... end. The initialize method is the constructor, called by Class.new. The conventional naming is PascalCase.
The class body is executable code run at class-definition time:
class Counter
puts "Counter class is being defined" # runs at class-definition
@@count = 0
def initialize
@@count += 1
end
end
The mechanism admits substantial metaprogramming; treated in Metaprogramming.
Instance variables
Properties are instance variables prefixed with @:
class Person
def initialize(name)
@name = name
end
def display
puts @name
end
end
Instance variables are not declared — assignment creates them. They are not visible from outside the class without an accessor. Reading an unset instance variable returns nil (with a warning).
Accessors
Manual accessor methods:
class Person
def initialize(name)
@name = name
end
def name
@name
end
def name=(value)
@name = value
end
end
p = Person.new("Alice")
puts p.name # "Alice"
p.name = "Bob" # calls name= setter
For convenience, the attr_* shortcuts:
class Person
attr_reader :name # generates name (getter)
attr_writer :name # generates name= (setter)
attr_accessor :age # generates both
end
The conventional contemporary form uses attr_accessor, attr_reader, attr_writer:
class Point
attr_accessor :x, :y # both readers and writers
def initialize(x, y)
@x = x
@y = y
end
end
p = Point.new(1, 2)
p.x # 1
p.x = 10
The attr_* methods are class methods that define instance methods at class-definition time. They are themselves Ruby methods (defined on Module); the conventional Ruby standard library uses them universally.
Class methods
Methods on the class itself (not on instances):
class Person
def self.create(name, age) # class method (preferred form)
new(name, age)
end
class << self
def from_hash(h) # also a class method (alternative form)
new(h[:name], h[:age])
end
end
end
p = Person.create("Alice", 30)
p2 = Person.from_hash(name: "Bob", age: 25)
The two forms (def self.foo and class << self) are equivalent; the def self.foo is conventional for single methods, class << self for grouping multiple class methods.
Inheritance
The < admits inheriting from a parent class:
class Animal
def initialize(name)
@name = name
end
def speak
"..."
end
end
class Dog < Animal
def speak
"Woof!" # override
end
def fetch
puts "#{@name} fetches"
end
end
d = Dog.new("Rex")
puts d.speak # "Woof!"
d.fetch
A class may have at most one parent — single inheritance. The default parent is Object (or BasicObject for the deepest possible base).
The super keyword admits calling the parent’s implementation:
class Cat < Animal
def speak
super + " (meow)" # call parent's speak
end
def initialize(name, breed)
super(name) # call parent's initialize
@breed = breed
end
end
The form super (no parens) passes the same arguments to the parent; super(args) passes specific arguments; super() passes none.
Visibility
Method visibility:
class Account
def initialize(balance)
@balance = balance
end
def deposit(amount) # public (default)
@balance += amount
log_transaction("deposit", amount)
end
private # everything below is private
def log_transaction(kind, amount)
puts "[#{kind}] #{amount}"
end
end
a = Account.new(100)
a.deposit(50) # OK
a.log_transaction("withdraw", 10) # NoMethodError: private
The three modifiers:
public— callable from anywhere (default).private— callable only with implicitself(no explicit receiver).protected— callable on any instance of the class or subclass.
The principal distinction: private admits no explicit receiver — even self.method fails (with one exception for setters: self.foo = value admits private setters since 2.7).
class Foo
private
def helper; end
def example
helper # OK
self.helper # ERROR
end
end
Ruby 3.0+ admits private def method_name:
class Foo
private def helper
# ...
end
end
The form is concise for single-method visibility.
self in classes
Inside a class body (outside any method), self is the class:
class Foo
puts self # Foo
def instance_method
puts self # the instance
end
def self.class_method
puts self # Foo
end
end
Inside an instance method, self is the receiver. Inside a class method, self is the class.
Constructors
The initialize method is called by Class.new:
class Person
def initialize(name, age = 0, role: "guest")
@name = name
@age = age
@role = role
end
end
p = Person.new("Alice", 30, role: "admin")
The Class.new allocates an instance and calls initialize on it. The initialize is private by default; it can only be called via Class.new.
Object identity and equality
a = Person.new("Alice")
b = Person.new("Alice")
a == b # false (default == is object identity)
a.equal?(a) # true (same object)
a.equal?(b) # false
# Override == for value equality:
class Person
attr_reader :name
def ==(other)
other.is_a?(Person) && name == other.name
end
def hash # required for Hash keys
name.hash
end
alias_method :eql?, :== # Hash uses eql?
end
The conventional discipline overrides ==, eql?, and hash together for value-typed classes.
to_s and inspect
The to_s produces the user-facing form; inspect produces the developer-facing form:
class Point
attr_reader :x, :y
def initialize(x, y)
@x = x
@y = y
end
def to_s
"(#{x}, #{y})"
end
def inspect
"#<Point x=#{x} y=#{y}>"
end
end
p = Point.new(1, 2)
puts p # "(1, 2)" (uses to_s)
p p # "#<Point x=1 y=2>" (uses inspect)
The conventional discipline:
to_s— forputs, string interpolation, and user output.inspect— for debugging and development tools (p,pp).
Comparable
The Comparable mixin admits ordering from <=>:
class Money
include Comparable
attr_reader :amount
def initialize(amount)
@amount = amount
end
def <=>(other)
amount <=> other.amount
end
end
a = Money.new(100)
b = Money.new(200)
a < b # true
a >= b # false
[a, b].max # b
a.between?(Money.new(50), Money.new(150)) # true
Treated in Modules and mixins.
Struct
The Struct class admits creating value-typed classes concisely:
Person = Struct.new(:name, :age) do
def greet
"Hello, #{name}"
end
end
p = Person.new("Alice", 30)
puts p.name # "Alice"
puts p.greet # "Hello, Alice"
puts p == Person.new("Alice", 30) # true (value equality)
The mechanism admits substantial conciseness for data-oriented classes; Struct provides:
- Constructor — accepts the listed attributes.
- Accessors — readers and writers.
- Equality — value-based
==,eql?,hash. - Iteration —
eachover the values.
For immutable structs, Data (Ruby 3.2+):
Point = Data.define(:x, :y)
p = Point.new(x: 1, y: 2)
puts p.x # 1
# p.x = 10 # NoMethodError (immutable)
The Data.define produces an immutable class; the conventional contemporary form for value objects.
Open classes
Existing classes may be reopened:
class String
def shout
upcase + "!"
end
end
"hello".shout # "HELLO!"
The mechanism admits monkey patching — modifying built-in classes. The conventional contemporary discipline reserves the technique for genuinely necessary cases (libraries, framework extensions); monkey-patching unrelated classes admits substantial action-at-a-distance.
For more disciplined extension, refinements admit lexically-scoped patches:
module StringShout
refine String do
def shout
upcase + "!"
end
end
end
class MyClass
using StringShout
# shout is admitted here
def example
"hello".shout # "HELLO!"
end
end
# shout is NOT admitted outside MyClass
Refinements are admitted but not widely used; treated in Metaprogramming.
Common patterns
Value object with Data (Ruby 3.2+)
Point = Data.define(:x, :y) do
def distance_to(other)
Math.sqrt((x - other.x) ** 2 + (y - other.y) ** 2)
end
end
a = Point.new(x: 0, y: 0)
b = Point.new(x: 3, y: 4)
puts a.distance_to(b) # 5.0
Service object
class CreateUser
def self.call(params)
new(params).call
end
def initialize(params)
@params = params
end
def call
user = User.new(name: @params[:name])
user.save!
UserMailer.welcome(user).deliver_later
user
end
end
user = CreateUser.call(name: "Alice")
The pattern admits substantial decoupling of business logic.
Builder pattern
class QueryBuilder
def initialize
@conditions = []
@order = nil
end
def where(condition)
@conditions << condition
self
end
def order(field)
@order = field
self
end
def build
sql = "SELECT *"
sql += " WHERE #{@conditions.join(' AND ')}" unless @conditions.empty?
sql += " ORDER BY #{@order}" if @order
sql
end
end
sql = QueryBuilder.new.where("active").where("role = 'admin'").order("created_at").build
Inheritance with template method
class Report
def generate
header
body
footer
end
def header
"==== #{title} ===="
end
def footer
"==== End ===="
end
def title
raise NotImplementedError
end
def body
raise NotImplementedError
end
end
class SalesReport < Report
def title
"Sales"
end
def body
"Sales data goes here"
end
end
Class with private setter
class Counter
attr_reader :count
def initialize
@count = 0
end
def increment
self.count = count + 1 # call setter (private after Ruby 2.7)
end
private
attr_writer :count # private setter
end
Comparable value type
class Version
include Comparable
attr_reader :major, :minor, :patch
def initialize(major, minor, patch)
@major = major
@minor = minor
@patch = patch
end
def <=>(other)
[major, minor, patch] <=> [other.major, other.minor, other.patch]
end
def to_s
"#{major}.#{minor}.#{patch}"
end
end
v1 = Version.new(1, 2, 3)
v2 = Version.new(1, 3, 0)
v1 < v2 # true
[v1, v2].sort # [v1, v2]
Dependency injection through constructor
class UserService
def initialize(repository:, mailer:)
@repository = repository
@mailer = mailer
end
def create(name)
user = @repository.create(name: name)
@mailer.welcome(user)
user
end
end
# Production:
service = UserService.new(repository: UserRepository.new, mailer: UserMailer.new)
# Test:
service = UserService.new(repository: FakeRepo.new, mailer: FakeMailer.new)
Module-namespaced classes
module API
class V1
class UsersController
def index
# ...
end
end
end
class V2
class UsersController
def index
# ...
end
end
end
end
API::V1::UsersController.new.index
The module nesting admits substantial namespace organisation.
A note on the conventional discipline
The contemporary Ruby OOP advice:
- Use
attr_*for accessors. - Use
Data.define(Ruby 3.2+) for immutable value objects. - Use
Structfor mutable value objects. - Use
Comparablemixin to derive ordering from<=>. - Use
privategenerously — keep public surface small. - Use
superto extend rather than replace. - Use composition over inheritance — favour modules/mixins or
has_arelationships. - Override
==,eql?,hashtogether for value equality. - Override
to_sfor user output;inspectfor development. - Avoid monkey-patching outside genuine library extension.
- Use refinements for scoped patches.
The combination — pure object-orientation, single-inheritance with mixins, attr_* accessors, Comparable and other built-in mixins, Struct/Data for value objects, the substantial introspection facilities — is the substance of Ruby’s OOP. The discipline produces expressive, well-encapsulated code with substantial flexibility through the open-class and metaprogramming facilities.