Menu
Coddy logo textTech

Guarding State

Part of the Object Oriented Programming section of Coddy's Ruby journey. Lesson 20 of 57.

An object should refuse requests that would break its state. raise stops the method before anything changes. Your own error class names the problem precisely; < StandardError makes the class a kind of error (inheritance, chapter 5):

class InsufficientFunds < StandardError
end

class Account
  attr_reader :balance

  def initialize(balance)
    @balance = balance
  end

  def withdraw(amount)
    raise InsufficientFunds, "only #{balance} available" if amount > balance

    @balance -= amount
  end
end

account = Account.new(50)
begin
  account.withdraw(80)
rescue InsufficientFunds => e
  puts "#{e.class}: #{e.message}"
end
puts account.balance

Output:

InsufficientFunds: only 50 available
50

Separate error classes let the caller react to each problem differently, with one rescue per class. Declare error classes before the classes that raise them:

class InvalidAmount < StandardError
end

class LimitReached < StandardError
end

class Card
  def charge(amount)
    raise InvalidAmount, "amount must be positive" unless amount.positive?
    raise LimitReached, "limit is 100" if amount > 100

    "charged #{amount}"
  end
end

[40, -5, 500].each do |amount|
  puts Card.new.charge(amount)
rescue InvalidAmount => e
  puts "fix the input: #{e.message}"
rescue LimitReached => e
  puts "try later: #{e.message}"
end

Output:

charged 40
fix the input: amount must be positive
try later: limit is 100

freeze turns an object read-only: after it, any change to its instance variables raises FrozenError, while reading still works. frozen? tells whether an object is frozen:

class Point
  attr_accessor :x

  def initialize(x)
    @x = x
  end
end

pt = Point.new(1).freeze
puts pt.frozen?
begin
  pt.x = 2
rescue FrozenError => e
  puts e.class
end
puts pt.x

Output:

true
FrozenError
1

A reader that returns an internal array hands out the real array, so outside code could change it. Return a frozen copy with @items.dup.freeze, or @items.dup, to keep the object in control of its own data:

class Playlist
  def initialize
    @songs = []
  end

  def add(song)
    @songs << song
  end

  def songs
    @songs.dup.freeze
  end
end

list = Playlist.new
list.add("Intro")
begin
  list.songs << "Hack"
rescue FrozenError
  puts "songs are read-only"
end
p list.songs

Output:

songs are read-only
["Intro"]
challenge icon

Challenge

Easy

Declare the error classes InvalidAmount and InsufficientFunds, both StandardError subclasses, and complete Account. deposit(amount) and withdraw(amount) raise InvalidAmount with amount must be positive for amounts below 1. withdraw raises InsufficientFunds with balance is only 20 when the amount is larger than the balance. Nothing changes when an error is raised.

The supplied code reads commands dep n or wd n and prints balance 70 after each, invalid: amount must be positive for InvalidAmount and refused: balance is only 20 for InsufficientFunds.

Your code goes in invalid_amount.rb, insufficient_funds.rb and account.rb. main.rb holds the supplied input/output code and cannot be edited.

Try it yourself

require_relative 'invalid_amount'
require_relative 'insufficient_funds'
require_relative 'account'

# Supplied input/output code: keep it as it is
input = []
while (line = gets)
  input << line.chomp
end
account = Account.new
input.each do |line|
  action, amount = line.split
  begin
    action == "dep" ? account.deposit(amount.to_i) : account.withdraw(amount.to_i)
    puts "balance #{account.balance}"
  rescue InvalidAmount => e
    puts "invalid: #{e.message}"
  rescue InsufficientFunds => e
    puts "refused: #{e.message}"
  end
end
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming

Practice on your own: Online Ruby compiler