概要
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