Tbpgr Blog

Employee Experience Engineer tbpgr(てぃーびー) のブログ

Ruby | Collecting Inputs | Document assumptions with assertions

概要

Document assumptions with assertions

前提

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

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

詳細

状況

メソッドは銀行取引のような外部システムからの入力データを受け取る。
入力フォーマットはドキュメント化されておらず、潜在的な危険性を持っている。

概要

入力データフォーマットに関するすべての仮定をアサーションによってドキュメント化しなさい。
データについてより理解するために表明の失敗を使うことで、フォーマットの変更があった時に警告を知らせてくれる。

理由

一貫性がなく、変わりやすく、ドキュメント化されていない外部システムからの入力情報を扱うとき、
警告もなく変化した入力を検出する「鉱山のカナリアとして」豊富な表明が役立ち、検証の役割も果たす。

サンプルコード仕様

何を返却してくるかわからない外部サービス「UnknownExternalSystem」に対して、表明を利用して
仕様を明らかにしつつ正しいプログラムに近づけていく。

表明は入力に対するドキュメントとしての役割を果たしつつ、想定外のデータがあれば
鉱山のカナリアとしてすぐに異常値を知らせてくれます。

サンプルコード その1

UnknownExternalSystemはunknown_methodはHashを返してくる、という予想のもと
表明を追加してみました。
結果、Arrayが返却されてきたためエラーが発生しました。

require 'pp'

class UnknownExternalSystem
  def unknown_method
    [{name: "tanaka", age: 54},{name: "suzuki", age: 54.5}]
  end
end

class MySystem
  def use_unknown_method
    ues = UnknownExternalSystem.new
    ret = ues.unknown_method
    fail TypeError, "not Hash (actual #{row.class})" unless ret.is_a? Hash
  end
end

ms = MySystem.new
ms.use_unknown_method

出力

 `use_unknown_method': not Hash (TypeError)

サンプルコード その2

UnknownExternalSystemはunknown_methodはArrayを返してくることが分かったので
Arrayに関する表明を追加します。
Arrayの中身はHashであると予想して表明を追加します。
予想通り、Hashが返却されてきたため次(その3)に進みます。

require 'pp'

class UnknownExternalSystem
  def unknown_method
    [{name: "tanaka", age: 54},{name: "suzuki", age: 54.5}]
  end
end

class MySystem
  def use_unknown_method
    ues = UnknownExternalSystem.new
    ret = ues.unknown_method
    fail TypeError, "not Array (actual #{ret.class})" unless ret.is_a? Array
    ret.each do |row|
      fail TypeError, "not Hash (actual #{row.class})" unless row.is_a? Hash
    end
  end
end

ms = MySystem.new
ms.use_unknown_method

サンプルコード その3

UnknownExternalSystemはunknown_methodはHashを要素に持つArrayを返してくることが分かりました。
次にHashの中身に関する表明を追加します。
nameキーが存在し、Stringを保持していることを予想し表明を追加します。
予想通りだったため次(その4)に進みます。

require 'pp'

class UnknownExternalSystem
  def unknown_method
    [{name: "tanaka", age: 54},{name: "suzuki", age: 54.5}]
  end
end

class MySystem
  def use_unknown_method
    ues = UnknownExternalSystem.new
    ret = ues.unknown_method
    fail TypeError, "not Array (actual #{ret.class})" unless ret.is_a? Array
    ret.each do |row|
      fail TypeError, "not Hash (actual #{row.class})" unless row.is_a? Hash
      name = row.fetch(:name)
      fail TypeError, "name is not String (actual #{name.class})" unless name.is_a? String
    end
  end
end

ms = MySystem.new
ms.use_unknown_method

サンプルコード その4

UnknownExternalSystemはunknown_methodはHashを要素に持つArrayを返してくることが分かりました。
Hashの中身にはnameキーでStringの値が保存されていることがわかりました。
次にageキーが存在し、Fixnumを保持していることを予想し表明を追加します。
Floatのデータが来たため、エラーになりました。
Floatにも対応する必要があるようです。

require 'pp'

class UnknownExternalSystem
  def unknown_method
    [{name: "tanaka", age: 54},{name: "suzuki", age: 54.5}]
  end
end

class MySystem
  def use_unknown_method
    ues = UnknownExternalSystem.new
    ret = ues.unknown_method
    fail TypeError, "not Array (actual #{ret.class})" unless ret.is_a? Array
    ret.each do |row|
      fail TypeError, "not Hash (actual #{row.class})" unless row.is_a? Hash
      name = row.fetch(:name)
      fail TypeError, "name is not String (actual #{name.class})" unless name.is_a? String
      age = row.fetch(:age)
      fail TypeError, "age is not Fixnum (actual #{age.class})" unless age.is_a? Fixnum
    end
  end
end

ms = MySystem.new
ms.use_unknown_method

出力

 `block in use_unknown_method': age is not Fixnum (actual Float) (TypeError)

サンプルコード その5

UnknownExternalSystemはunknown_methodはHashを要素に持つArrayを返してくることが分かりました。
Hashの中身にはnameキーでStringの値が保存されていることがわかりました。
Hashの中身にはnameキーでFixnumとFloatの値が保存されていることがわかりました。
Floatの表明を追加します。

require 'pp'

class UnknownExternalSystem
  def unknown_method
    [{name: "tanaka", age: 54},{name: "suzuki", age: 54.5}]
  end
end

class MySystem
  def use_unknown_method
    ues = UnknownExternalSystem.new
    ret = ues.unknown_method
    fail TypeError, "not Array (actual #{ret.class})" unless ret.is_a? Array
    ret.each do |row|
      fail TypeError, "not Hash (actual #{row.class})" unless row.is_a? Hash
      name = row.fetch(:name)
      fail TypeError, "name is not String (actual #{name.class})" unless name.is_a? String
      age = row.fetch(:age)
      fail TypeError, "age is not Fixnum (actual #{age.class})" unless [Fixnum, Float].include? age.class

      # some main logic
    end
  end
end

ms = MySystem.new
ms.use_unknown_method