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

Nullオブジェクトを使ってnil値のチェックをなくす方法

More than 1 year has passed since last update.

はじめに

こんにちは! みんなのウェディングのエンジニア@kiyokuroです。 この記事はくふうカンパニーアドベントカレンダーの15日目になります。

今回はNullオブジェクトを使ってnil値のチェックを省略した実装について書きたいと思います。

例えば、必ずしもログインを要求しないアプリケーションでは、ユーザが存在しない(nil)こともしばしばあると思います。ユーザが存在しない時は何も表示しないのであれば問題ないですが、「名無しのユーザ」と言ったユーザが存在しないときには決まった値を表示する要件もあると思います。
そんなオブジェクトの存在性によって仕様がある時の対処方法を説明します。

Nullオブジェクトとは

オブジェクトのフィールドへの問い合わせ時にnilチェックを行い、クライアントがフィールドに問い合わせる時にnilが含まれることを意識しなくて済むようにするためのパターンです。

使用例

今回の仕様は以下のものとします。
- ログインしていたらユーザ名とユーザのIDを表示する
- ログインしていなかったら「名無しのユーザ」と言う名前とブラウザーIDを表示する

Nullオブジェクトを使わないとき

nilチェックをすると次のようになると思います。
例えばviewでユーザ名とIDを表示する時。

- if @user
  = @user.name     -# => kiyokuro
  = @user.user_id  -# => 1234
- else
  名無しのユーザ
  @browser_id      -# => 1234567890

このようにcurrent_userのフィールドにnilを許容しているものが多いと、表示処理の度にnilチェックが必要になりコード量が増え複雑になっていきます。

Nullオブジェクトを使う

そこでNullオブジェクトを使います。
まずはuserがnilの時のNullユーザオブジェクトとしてNullUserを作ります。
Userクラスを継承置き換えるフィールド以外はUserクラスとして振る舞うようにします。

class NullUser < User
  def id
    browser_id
  end

  def name
    "名無しのユーザ"
  end
end

次にUserクラスにNullUserオブジェクトを生成するファクトリメソッドを追加します。
そしてUserがnilであればNullUserを返すようにします。

class User < ApplicationRecord
  def self.find(id)
    find_by(id: id) || null_user
  end

   private

    def self.null_user
      NullUser.new
    end
end

これで検索したUserが存在すればUserを返し、存在しなければNullUserを返すようになりました。

User.find(1) #存在するUserのID
=> #<User:
id: 1,
name: "kiyokuro">

User.find(2) #存在しないUserのID
-> #<User:
id: 1234567890
name: "名無しのユーザ">

クライアントのリクエストに対してUserの存在性に関係なく値を返せるようになったので、viewやmodelで条件記述を省くことができるようになりました。
上記の例ではfindメソッドでActiveRecord::RecordNotFoundの例外が発生しなくなるため、アプリケーション内のクライアントでは上記の仕様を適用する場合でないと使用できません。

全てのクライアントが同じ返答を期待しないとき

Userが存在するか否かで別の振る舞いを期待するクライアントがある時は、ユーザがUserクラスであるかNullUserであるか判定できるようにします。
UserクラスとNullUserクラスにis_nil?メソッドを実装してUserクラスであればfalse、NullUserクラスであればtrueを返すようにします。

class User < ApplicationRecord
  def is_nil?
    false
  end

  def self.find(id)
    find_by(id: id) || null_user
  end

   private

    def self.null_user
      NullUser.new
    end
end
class NullUser < User
  def is_nil?
    true
  end

  def id
    browser_id
  end

  def name
    "名無しのユーザ"
  end
end

これでUserの存在性を判定して処理を追加できます。

if @user.is_nil?
  # 会員登録ページにリダイレクト
else
  # マイページにリダイレクト
end

最後に

いつでもどこでも使える便利な方法という訳ではありませんが、viewやモデルで存在性判定の条件記述が増えてきた時には利用できないか検討できるのではないかと思います。

参考書籍

新装版 リファクタリング 既存のコードを安全に改善する
https://www.amazon.co.jp/dp/B01IGW5MG0/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1

明日は

くふうカンパニーアドベントカレンダー15日目はKotaSakuraiさんです

kiyokuro
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
ユーザーは見つかりませんでした