Classes and objects

Ruby · Essentials · 14 min

Ruby is object-oriented to the core, and every gem and Rails app is built from classes.

Define a class with class Name ... end and create instances with Name.new(args), which calls initialize. Instance variables (@name) hold an object's state; they are always private to the object and spring into existence (as nil) on first use.

Expose state with accessor macros, which generate methods for you:

  • attr_reader :name defines name.
  • attr_writer :name defines name=.
  • attr_accessor :name defines both.

Inside methods, call setters with self.name = x. Without self., Ruby creates a local variable instead, a classic bug.

Visibility: methods are public by default; everything after private is callable only without an explicit receiver (from inside the object). protected allows calls from other instances of the same class.

Class methods are defined with def self.create, and class-level constants with NAME = .... Inheritance uses <: class Admin < User, and super calls the parent's version of the current method (bare super forwards all arguments).

Useful hooks: define to_s for puts and string interpolation, inspect for p, and == for value equality. Include Comparable and define <=> to get <, >, between? and clamp for free.

For simple data holders, Data.define(:x, :y) (Ruby 3.2) creates an immutable value class with keyword or positional construction and value equality.

Example

class Account
  attr_reader :owner, :balance

  def initialize(owner, balance = 0)
    @owner = owner
    @balance = balance
  end

  def deposit(amount)
    raise ArgumentError, "amount must be positive" unless amount.positive?
    @balance += amount
    self
  end
  def to_s = "#{owner}: #{balance}"
end

acct = Account.new("Ada").deposit(50).deposit(25)
puts acct                # Ada: 75
p acct.respond_to?(:balance=)   # false: read-only

Point = Data.define(:x, :y)
a = Point.new(x: 1, y: 2)
p a                      # #<data Point x=1, y=2>
p a == Point.new(1, 2)   # true: value equality
p a.with(y: 5)           # #<data Point x=1, y=5>

Remember

  • initialize sets up @instance_variables; Name.new calls it
  • attr_reader, attr_writer and attr_accessor generate accessors
  • Use self.attr = inside methods to call a setter

Watch out for

  • Writing balance = 10 inside a method meaning the setter; it creates a local variable. Use self.balance = 10.

Check yourself

  1. Inside an instance method, what does name = "x" do when attr_accessor :name is defined?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.ruby-lang.org/en/3.4/Class.html