Menu
Coddy logo textTech

状態を保護する

CoddyのRubyジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 20/57。

オブジェクトは、自身の状態を壊すような要求を拒否すべきです。raiseは、何も変更される前にメソッドを停止します。独自のエラークラスによって問題を正確に名付け、< StandardErrorによってそのクラスをエラーの一種にします(継承、第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

出力:

InsufficientFunds: only 50 available
50

エラークラスを分けると、呼び出し側はクラスごとに1つの rescue を使って、それぞれの問題に異なる方法で対応できます。エラーを発生させるクラスより前に、エラークラスを宣言します。

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

出力:

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

freezeはオブジェクトを読み取り専用にします。実行後は、インスタンス変数を変更しようとするとFrozenErrorが発生しますが、読み取りは引き続き機能します。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

出力:

true
FrozenError
1

内部配列を返すリーダーは実際の配列をそのまま渡すため、外部のコードによって変更される可能性があります。オブジェクト自身のデータをオブジェクトが管理し続けられるように、@items.dup.freeze または @items.dup を使って凍結したコピーを返します。

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

出力:

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

チャレンジ

簡単

エラークラス InvalidAmount と InsufficientFunds を宣言します。どちらも StandardError のサブクラスです。また、Account を完成させます。deposit(amount) と withdraw(amount) は、1 未満の金額に対して amount must be positive を伴う InvalidAmount を発生させます。金額が残高を超える場合、withdraw は balance is only 20 を伴う InsufficientFunds を発生させます。エラーが発生した場合、何も変更されません。

提供されたコードは dep n または wd n のコマンドを読み取り、それぞれの後に balance 70 を出力します。また、InvalidAmount の場合は invalid: amount must be positive を、InsufficientFunds の場合は refused: balance is only 20 を出力します。

コードは invalid_amount.rb、insufficient_funds.rb、および account.rb に記述します。main.rb には提供された入出力コードが含まれており、編集できません。

自分で試してみよう

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

# 提供された入出力コード: そのままにしておく
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 icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン

自分で練習してみよう: Rubyオンラインコンパイラ