【Rails】『nilの場合は~』のリファクタリング5パターン

Ruby

Railsアプリケーションで『nilの場合は~』を実装する際のリファクタリングについて紹介します。
今回は以下のようなパターンに分け、それぞれについて解説します。

nilに関するリファクタリングパターン
  • 『変数がnilの場合はデータをセットする』パターン
  • 『値がnilの場合は別の値を変数に代入する』パターン
  • 『オブジェクトがnilの場合はプロパティを参照しない』パターン
  • 『オブジェクトがnilの場合は別のプロパティを代入する』パターン
  • 『値がnilの場合はnil、nilでなければ評価式の結果を返す』パターン

『変数がnilの場合はデータをセットする』パターン

変数を利用するにあたり、もし変数がnilだったら別の値をセットするというケースです。

たとえば以下のようなコードがリファクタリング対象です。

### nilチェックを利用
def name
  if @name.nil?
    # @nameがnilだったら@nameの代わりに'anonymous'を利用する
    'anonymous'
  else
    @name
  end
end
### 後置ifを利用
def name
  @name = 'anonymous' if @name.nil?
  @name
end
### OR演算子を利用
def name
  @name = @name || 'anonymous'
end

自己代入演算子(||=)を利用する

自己代入演算子(||=)で書き換えられます。
リファクタリング後のコードは以下の通りです。

def name
  @name ||= 'anonymous'
end

なお、自己代入演算子を利用したインスタンス変数の遅延初期化もこのパターンに該当します。

def my_client
  # 初回アクセス時、@my_clientはnilなので初期化
  # 2回目以降のアクセス時、既に初期化された@my_clientを利用する
  @my_client ||= MyClient.new
end

複数行で初期化をする場合はtapを組み合わせる

複数行にわたる初期化を自己代入演算子を利用して遅延させたい場合はtapと組み合わせます。
tapは『selfを引数としてブロックを評価する』『返す値はブロックの評価値ではなくself』という特徴をもつメソッドです。1
tapの詳細解説はtapメソッドとは?tapの具体的な使い方の紹介で紹介しています。

自己代入演算子とtapを組み合わせたリファクタリング例は以下の通りです。

### リファクタリング前
class MyLogger
  def self.logger
    return @logger if @logger

    # ログを標準出力する
    @logger = ActiveSupport::Logger.new(STDOUT)

    # ログのフォーマッタをセットする
    @logger.formatter = Logger::Formatter.new

    # タイムスタンプのフォーマットを設定する
    @logger.formatter.datetime_format = '%Y-%m-%d %H:%M:%S'

    # 設定が完了したインスタンスを返す
    @logger
  end
end
### リファクタリング後
class MyLogger
  def self.logger
    # 初期化の処理がブロック内で完結する
    @logger ||= ActiveSupport::Logger.new(STDOUT).tap do |logger|
      logger.formatter = Logger::Formatter.new
      logger.formatter.datetime_format = '%Y-%m-%d %H:%M:%S'
    end
  end
end

tapはProcやbegin式でも代替可能です。各メソッドによる初期化方法の詳細はProc/begin式/tapによるインスタンス生成方法の比較で解説しています。

『値がnilの場合は別の値を変数に代入する』パターン

present?の結果によって代入する値を決めるケースです。

たとえば以下のようなコードがリファクタリング対象です。

if items.present?
  list = items
else
  list = []
end

present?の代わりにpresenceとOR演算子(||)を利用する

presenceは『オブジェクトが存在する場合はオブジェクト自身、存在しない場合はnil』を返すメソッドです。
presenceとOR演算子(||)による短絡評価(ショートサーキット)を組み合わせることでpresent?を利用したコードが簡潔になります。
短絡評価とは『左辺を評価した段階で式の結果が決まらない場合のみ右辺を評価する(式の結果が決まった時点で後続の評価をしない)』という評価方法のことです。

リファクタリング後のコードは以下の通りです。

### itemsはnilの可能性もある配列
list = items.presence || []

present?presenceの違いの詳細解説は【Rails】present?とpresenceの違いと使いどころで紹介しています。

空文字(””)や空配列([])に対するpresenceの結果はnilなので注意

空文字や空配列とnilを明確に区別したい場合、presenceを利用すると意図した結果にならないので注意が必要です。

以下は意図した結果にならないコード例です。

### 空文字であろうと@nameに値がセットされている時は@nameを参照したい場合、以下のコードでは意図した結果にならい。
@name = ""
name = @name.presence || 'anonymous'
=> "anonymous" # 本当は空文字が返されて欲しい

nilと明確に区別したい場合はnil?を利用します。

@name = ""
name = @name.nil? ? 'anonymous' : @name
=> ""

配列の要素に対して処理を行う場合はArrayを利用するとより簡潔になる

配列の要素に対する処理はpresenceを利用して以下のように簡潔に書き換えられます。

### リファクタリング前
def item_ids(items)
  if items.present?
    items.map(&:id)
  else
    []
  end
end
### リファクタリング後
def item_ids(items)
  # 条件分岐が不要になる
  # itemsの有無に関わらずmapの処理ができる
  list = items.presence || []
  list.map(&:id)
end

nilの可能性がある配列に対して処理を行う際は、nilを空配列([])として扱うArrayを利用することでより簡潔にかけます。

item_ids = Array(items).map(&:id)

『オブジェクトがnilの場合はプロパティを参照しない』パターン

NoMethodErrorを防ぐためにオブジェクトが存在していることを確認してからプロパティを参照するケースです。

たとえば以下のようなコードがリファクタリング対象です。

if user.present? && user.name == 'Tanaka'
  p 'Hello Tanaka'
end

tryあるいは『&.』を利用する

tryあるいは&.(ぼっち演算子、Null条件演算子、Safe Navigation Operator)を利用すると簡潔にかけます。

### tryを利用した場合
if user.try(:name) == 'Tanaka'
  p 'Hello Tanaka'
end

### &.を利用した場合
if user&.name == 'Tanaka'
  p 'Hello Tanaka'
end

ハッシュの場合はdigを利用するとよい

ハッシュの場合はtry hashを利用することでNoMethodErrorを防げますが、digも代替可能なので覚えておくとよいでしょう。

### nilオブジェクトのプロパティを参照する(通常の参照)
> user = nil
> user[:name]
# NoMethodError: undefined method `name' for nil:NilClass

### nilオブジェクトのプロパティを参照する(try hashによる参照)
> user = nil
> user.try(:[], :name)
=> nil

### nilオブジェクトのプロパティを参照する(digによる参照)
> user = nil
> user.dig(:name)
=> nil

digの利用方法の詳細解説は【Ruby】ハッシュのtry(try hash)をdigで書き換えるで紹介しています。

『オブジェクトがnilの場合は別のプロパティを代入する』パターン

たとえば以下のようなコードがリファクタリング対象です。

if user.present?
  user_name = user.name
else
  user_name = 'anonymous'
end

tryとOR演算子を利用する

オブジェクトのnilを考慮に入れてプロパティを代入する場合はtryとOR演算子を組み合わせることで簡潔にかけます。

リファクタリング後のコードは以下の通りです。tryの代わりに&.も利用可能です。

### tryを使う場合
user_name = user.try(:name) || 'anonymous'

### &.を使う場合
user_name = user&.name || 'anonymous'

値がnilの場合はnil、nilでなければ評価式の結果を返す

値が存在する場合のみ式を評価したいケースです。たとえば以下のようなコードがリファクタリング対象です。

def is_even(value)
  if value.nil?
      nil
  else
    value.even?
  end
end

### 実行結果
> is_even(2)
=> true

> is_even(5)
=> false

> is_even(nil)
=> nil

&&(論理積)を利用する

&&(論理積)を利用すると簡潔にかけます。

def is_even(value)
  value && value.even?
end

### 実行結果
> is_even(2)
=> true

> is_even(5)
=> false

> is_even(nil)
=> nil

さいごに

Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!

参考記事