出会いは突然に😲
いつものように眠い目を擦りながらコードレビューをしていると以下のようなコードに遭遇しました。
(※部分的に書き換えてあるので、実際には動作していないコードです)
def fetch_companies(setting_id) setting = Setting.find_by(id: setting_id) if setting companies = Company.where(setting.kind) companies end end
処理としては指定されたid
のSetting
データを取得して、Setting
に記録されているkind
を持つCompany
の一覧を取得するという単純なものです。ActiveRecord(O/RMapper)
を使ったデータベースとのやりとりを行う処理です。
動作自体には何の問題もないのですが、この関数は指定したid
を持つ、Setting
の値が見つかった時と見つからなかった時で返す値の型が異なります。
Ruby
やElixir
では明示的にreturn
構文を書かなくても最終行に記述されている値が自動で返ります。現時点では以下のように値が返ります。
Setting
が見つかった時 ->Company
の配列 もしくは空配列where
は1件もヒットしなかった場合に空配列を返します
Setting
が見つからなかった時 ->nil
def verification end res = verification p res # nil
この関数に型アノテーションをしてあげるとすると[Company] | nil
という感じになります。
何が問題なのか🤔
状態によって返る値の型が異なることが分かりましたが、これがどのような問題につながるでしょうか。
オブジェクト指向をサポートする言語でよくある例としてはfetch_companies
の戻り値に対してmap
やfilter
を適応したい場合です。
resp_companies = fetch_companies(6) extract_names = resp_companies.map { |company| company.name }
このコードはSetting
が存在していた場合にCompany
の配列が返ってきてくれた場合には上手く動作してくれますが、Setting
が見つからなかった場合にはどうでしょうか。先ほど記述したように、その場合にはnil
が返ってきます。nil
にはmap
関数は実装されていないので、当然ですがnil::NilClass
エラーになります。
nil.map { |company| company.name } # undefined method `map' for nil:NilClass (NoMethodError)
この問題を回避するために、fetch_companies
の呼び出し元で戻り値を精査する必要があります。
resp_companies = fetch_companies(6) return [] if resp_companies.empty? extract_names = resp_companies.map { |company| company.name }
fetch_companies
を呼び出す箇所が少なければ良いのでしょうが、たくさんあったとするならば、この単調な判定を各所に記述するのは面倒です。
解決策📖
解決方法はシンプルで静的型付け言語で当たり前のように行う、関数の戻り値の型を統一してあげれば良いです。
今回の場合では空配列を返すのが適切です。これでSetting
の有無に関わらず、全ての場合で空配列か、Company
の配列が返るようになります。
# fetch_companies :: number -> [Company] def fetch_companies(setting_id) setting = Setting.find_by(id: setting_id) return [] if setting.nil? Company.where(setting.kind) end
心なしかif
のネストも無くなりコードの見栄えも良くなりました。必ず配列が返るので先程の問題は発生しません。
合わせて、呼び出し元でnil
が返ってくる場合を考えなくて良くなりました。やりました。関数の戻り値の型を揃えるのは良いことだらけです🎉