Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
89
Help us understand the problem. What is going on with this article?
@jnchito

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

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

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

89
Help us understand the problem. What is going on with this article?
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
jnchito
株式会社ソニックガーデンのRubyプログラマ。 「プロを目指す人のためのRuby入門( https://bit.ly/3wmJheK )」の著者。 および「Everyday Rails - RSpecによるRailsテスト入門 ( https://bit.ly/3rKqrKX )」の翻訳者。 プログラミングスクール「フィヨルドブートキャンプ」のメンターでもある。
sonicgarden
「お客様に無駄遣いをさせない受託開発」と「習慣を変えるソフトウェアのサービス」に取り組んでいるソフトウェア企業

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
89
Help us understand the problem. What is going on with this article?