167
91

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-12-01

はじめに: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を考慮すべきタイミングに限って使用する
  • &.を連発してしまったら、自分の設計や実装を疑ってみる

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

167
91
12

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
167
91

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?