Polyglot
Languages Ruby oop
Ruby § oop

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 implicit self (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 — for puts, 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.
  • Iterationeach over 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 Struct for mutable value objects.
  • Use Comparable mixin to derive ordering from <=>.
  • Use private generously — keep public surface small.
  • Use super to extend rather than replace.
  • Use composition over inheritance — favour modules/mixins or has_a relationships.
  • Override ==, eql?, hash together for value equality.
  • Override to_s for user output; inspect for 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.