Help us understand the problem. What is going on with this article?

【アンチパターン】全部nil(null)かもしれない症候群

More than 1 year has passed since last update.

はじめに: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?)で、friendnilでないことが保証されているからです(にもかかわらず、&.をおまじないのように付けまくる人はよく見かけます)。

しかし、それを無視したとしても、まだ&.が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を考慮すべきタイミングに限って使用する
  • &.を連発してしまったら、自分の設計や実装を疑ってみる

と考えるようにしてください。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした