Lesson 7 / 25

Classes, Attributes and Initialisation

Define classes with initialize, attribute accessors and useful conversion methods.

Defining your own objects

Define a class with class Invoice ... end. initialize runs when you call Invoice.new(...) and usually sets instance variables (@number), which are private to the object. Expose them with attr_reader (getter), attr_writer (setter) or attr_accessor (both); prefer readers and explicit methods over writers, so objects stay valid. Methods return their last expression. Define to_s for a human-readable string and rely on inspect for debugging output. Class methods are defined with def self.from_csv(row), useful for alternative constructors. Inside a method, self is the current object. Methods ending in ? return booleans by convention (paid?), and methods ending in ! are "dangerous" variants (mutating or raising). Equality is customisable by defining ==, eql? and hash, and <=> plus include Comparable gives you <, >, between? and clamp. Since Ruby 3.0, short methods can use endless method syntax: def total = items.sum(&:price).

An object's state and behaviour

Instance variables hold private state; public methods expose behaviour and selected data.

A rounded object box with small private cells inside and a few labelled handles on its border.
Figure 3.1 — Instance variables behind public methods.

An invoice class

Readers, predicates, a class-level constructor and an endless method.

class Invoice
  attr_reader :number, :items

  def initialize(number, items = [])
    @number = number
    @items = items
    @paid_at = nil
  end

  def self.from_hash(data)                     # alternative constructor
    new(data.fetch(:number), data.fetch(:items, []))
  end

  def total = items.sum { |i| i[:price] * i[:qty] }   # endless method (Ruby 3.0+)

  def paid? = !@paid_at.nil?

  def mark_paid!(at = Time.now)
    raise "Invoice #{number} already paid" if paid?
    @paid_at = at
    self
  end

  def to_s = "Invoice #{number}: Rs #{format('%.2f', total)}"
end

inv = Invoice.from_hash(number: "INV-7", items: [{ price: 49.5, qty: 2 }, { price: 1200, qty: 1 }])
puts inv            # Invoice INV-7: Rs 1299.00
inv.mark_paid!
puts inv.paid?      # true

Avoid attr_accessor by default

attr_accessor lets any code set any value, bypassing your rules. Start with attr_reader, and add methods such as mark_paid! that change state while checking invariants.

Quick check: What does attr_reader :number create?

  • A getter method number that returns @number
  • A setter only
  • A class variable
  • A constant
Answer

A getter method number that returns @number — attr_reader defines a reader method for the instance variable.