はじめに:Rubyの&.演算子やtryメソッドについて
Rubyには&.という演算子があります。
この演算子を使うと、メソッドのレシーバがnil(他の言語でいうところのnull)であっても、メソッド呼び出し時にエラーが起きません。(nilが返る)
# ユーザが見つかる場合
user = find_user('Alice')
user&.name #=> "Alice"
# ユーザが見つからない場合(userがnilの場合)
user = find_user('Foo')
user&.name #=> nil
同じようなメソッドに、Railsのtryメソッドがあります。
# ユーザが見つかる場合
user = find_user('Alice')
user.try(:name) #=> "Alice"
# ユーザが見つからない場合(userがnilの場合)
user = find_user('Foo')
user.try(:name) #=> nil
&.演算子やtryメソッドを多用しすぎることの問題
&.やtryを使うと、nilかどうかの条件分岐を書かなくて済むので、非常に便利です。
ですが、こうしたテクニックを多用しすぎると、奇妙なコードを作り出す原因になります。
たとえば次のようなコードです。
def find_friend_message(name)
friend = self.user&.friends&.find_by(name: name)
if friend&.message.blank?
'no message'
else
friend&.message
end
end
上のコードでは&.が4箇所も出てきます。
とりあえず、一番最後に出てくるfriend&.messageの&.は不要です。
なぜなら上の条件分岐(if friend&.message.blank?)で、friendがnilでないことが保証されているからです(にもかかわらず、&.をおまじないのように付けまくる人はよく見かけます)。
しかし、それを無視したとしても、まだ&.が3箇所も出てきます。
&.だとさらっと書けてしまうので、問題が見えづらいですが、上のコードは次のようなコードを書いていることと同じです。
def find_friend_message(name)
unless self.user.nil?
unless self.user.friends.nil?
friend = self.user.friends.find_by(name: name)
unless friend.nil?
unless friend.message.blank?
return friend.message
end
end
end
end
'no message'
end
こんなロジック見せられたら、普通は「うえっ」ってなりますよね。
でも&.によって消臭されてるだけで、実際やってることはこれと同じなわけです。
オブジェクトがnil/nullになる可能性がある言語は、大なり小なり、「nil/nullに対してメソッドを呼びだして爆死💥☠️」のリスクがあります。
しかし、そのリスクを過剰に恐れてしまうと、上のコードのように「これはnilかもしれない、これもnilかもしれない、全部nilかもしれない・・・!!!」と思って、&.やtryを連発してしまうことになります。
いわば「全部nil(null)かもしれない症候群」です。
この問題の処方箋
「全部nil(null)かもしれない症候群」を治すためには、次のようなアプローチを取るのが良いでしょう。
モデルやメソッドの設計をきちんと確認する
まず、&.やtryを使う前に「これはnilかもしれない」ではなく、「ここはnilが来る可能性が十分ある」なのか、「nilは通常あり得ない」なのかをハッキリさせる必要があります。
そのためにシステムのデータ構造(DB設計やDB制約、バリデーション処理等)や、呼びだしたメソッドの戻り値をきちんと確認して、nilが来るのか、来ないのかをハッキリさせましょう。
エラーを恐れない
「nilは通常あり得ない」のであれば、やみくもに&.やtryを付けるのはやめましょう。
通常あり得ないのに、nilがやってきたのであれば、それは異常事態です。
実行時エラーを発生させてプログラムを停止させ(&.を使わなければエラーが発生します)、nilがやってきた原因を調査すべきです。
下手にnilを許容すると、そこで問題が起きなくても、他の場所で別の問題を引き起こす恐れがあります。
また、コードを読んだ人間も「ここ、&.を使ってるけど、nilのケースがあるのかな・・・??」と首をかしげることになります。
つまり、一種の可読性の低下につながります。
ガード条件をうまく使う
「nilがありえる、なおかつ、nilでないときだけ処理したい」という場合は、&.を連発するよりも、ガード条件を使った方が可読性がよくなることがあります(まあ条件分岐が多い問題は解決してないのですが)。
def find_friend_message(name)
no_msg = 'no message'
return no_msg if self.user.nil?
friend = self.user.friends.find_by(name: name)
return no_msg if friend.nil?
friend.message.blank? ? no_msg : friend.message
end
そもそもの実装を見直す
もし、&.を連発しているのであれば、そもそも何か根本的な間違いをしている可能性もあります。
クラス設計やメソッドの責務を見直すことで、きれいな実装に直せるかもしれません。
まとめ
&.演算子やtryメソッドは便利ですが、多用しすぎると逆にデメリットが出てきます。
「全部&.を付けておけば、エラーが起きないからあんしーん!」ではなく、
- 原則、
&.は付けない。本当にnilを考慮すべきタイミングに限って使用する -
&.を連発してしまったら、自分の設計や実装を疑ってみる
と考えるようにしてください。