読者です 読者をやめる 読者になる 読者になる

Tbpgr Blog

Ruby プログラマ tbpgr(てぃーびー) のブログ

Ruby | Collecting Inputs | Reject unworkable values with preconditions

概要

Reject unworkable values with preconditions

前提

Confident Rubyではメソッド内の処理を次のように分類しています。
・Collecting Inputs(引数チェック、変換など)
・Performing Work(主処理)
・Delivering Output(戻り値に関わる処理)
・Handling Failure(例外処理)

当記事は上記のうち、Collecting Inputsに関する話です。

詳細

状況

いくつかの入力値は利用可能な形式に変換することができない。
それらを受け入れることは、潜在的に有害でデバッグを困難にする側面がある。

例えば、Employeeオブジェクトへのnilの雇用日は予期しないメソッドの動作を生むかもしれない。

概要

前提条件説(precondition clauses)を利用して早い段階で受入不可能な値を拒否する。

理由

すぐに失敗する方が、部分的に成功して混乱した例外を引き起こすよりも良い

前提条件説(precondition clauses)について

・不正な入力を事前に防ぐ
・各メソッドの処理前に前提条件説(precondition clauses)を確認することで、期待される入力値のドキュメントの役割も果たす

サンプルコード仕様(修正前)

Personクラスに名前と年齢を保持。
振る舞いは
大人かどうか返却する:adult?
年齢を説明する:explain_age
さばを読む:fudge_the_count
の3種類です。

メソッドはおそらく別々の開発者が作成した関係で、ageに対する入力値の扱いがことなります。
このような不統一はバグの元になる可能性が大きいです。

サンプルコード

class Person
  attr_reader :name, :age

  def initialize(name, age)
    @name, @age = name, age
  end

  def adult?
    fail "invalid" unless @age
    @age >= 20
  end

  def explain_age
    @age ? "I'm #{@age} years old" : ""
  end

  # さばを読む
  def fudge_the_count
    (@age / 10).ceil * 10
  end
end

tanaka = Person.new("tanaka", 15)
puts tanaka.adult?
puts tanaka.explain_age
puts tanaka.fudge_the_count

puts "-------------------------"

suzuki = Person.new("suzuki", 25)
puts suzuki.adult?
puts suzuki.explain_age
puts suzuki.fudge_the_count

出力

false
I'm 15 years old
10
-------------------------
true
I'm 25 years old
20

サンプルコード仕様(修正後)

Personクラスに名前と年齢を保持。
振る舞いは
大人かどうか返却する:adult?
年齢を説明する:explain_age
さばを読む:fudge_the_count
の3種類です。

ここまでは修正前と変わりませんが、前提条件説(precondition clauses)を利用して
コンストラクタの処理で不正な入力を除外します。
そのため、各メソッドでは不正な値について気にする必要がありません。
また、入力値に対する対応も一環しています。
きいです。

サンプルコード

class Person
  attr_reader :name, :age

  def initialize(name, age)
    @name= name
    self.age = age
  end

  def age=(age)
    fail 'invalid age nil' if age.nil?
    fail 'invalid age not have to_i' unless age.respond_to?(:to_i)
    @age = age.to_i
  end

  def adult?
    @age >= 20
  end

  def explain_age
    "I'm #{@age} years old"
  end

  # さばを読む
  def fudge_the_count
    (@age / 10).ceil * 10
  end
end

tanaka = Person.new("tanaka", 15)
puts tanaka.adult?
puts tanaka.explain_age
puts tanaka.fudge_the_count

puts "-------------------------"

suzuki = Person.new("suzuki", 25)
puts suzuki.adult?
puts suzuki.explain_age
puts suzuki.fudge_the_count

出力

false
I'm 15 years old
10
-------------------------
true
I'm 25 years old
20