🕌

Active RecordとNull Objectパターン

2021/06/22に公開1

Active RecordでNull Objectパターンを使うことはできるのか考えてみました。

Null Objectパターン

Null Objectパターンは、値が存在しない場合の処理を共通化したい場合に使えるデザインパターンです。

RubyでNull Objectパターンを利用する

CutomerクラスとLicenseクラスを使って、CustomerのLicenseのname要素を表示することを考えます。CustomerがLicenseを持っているとします。Licenseは空でも良いです。

class Customer
  attr_reader :license

  def initialize(license=nil)
    @license = license
  end
end

class License
  attr_reader :name

  def initialize(name)
    @name = name
  end
end

Coustomerの持っているLicenseの名前を表示したいときは、次のようなコードを書くことになります。

driving_license = License.new('運転免許')
customer = Customer.new(driving_license)
puts customer.license.name # => 運転免許

ここで、免許が無いときは、「免許なし」と表示したいとします。
おそらく最も単純な方法は、表示時に分岐を作ることです。

driving_license = License.new('運転免許')
customer1 = Customer.new(driving_license)
customer2 = Customer.new
puts customer1.license? ? customer1.license.name : '免許なし' # => 運転免許
puts customer2.license? ? customer2.license.name : '免許なし' # => 免許なし

このコードをいろいろな場所から使おうと思うとき、下記の懸念点があります。

  • Customerを利用する場所では、lisenceが設定されていなかった場合の分岐を書かなくては行けない
  • lisenceが設定されていなかった場合にどのような処理が走るのかは、利用する側のコードを見ないとわからない

こういう問題を解消したいとき、Null Objectパターンが有効に働くことがあります。
https://vain0x.github.io/blog/2016-08-18/null-object-patterns/

存在しないことを表現するLicenseクラス、NullLicenseを作ります。Customerでは、licenseが参照された際にlicenseがない場合は、NullLicenseのインスタンスを返すようにします。

class NullLicense
  def name
    '免許なし'
  end
end

class Customer
  def initialize(license=nil)
    @license = license
  end

  def license
    @license || NullLicense.new
  end
end

これで、licenseを参照する側で分岐を書く必要がなくなりました。

driving_license = License.new('運転免許')
customer1 = Customer.new(driving_license)
customer2 = Customer.new
puts customer1.license.name # => 運転免許
puts customer2.license.name # => 免許なし

毎回インスタンスを生成しなくて良くする

Customer#licenseは、コールされるたびにNullLicense.newを生成することになるので、無駄があります。NullLicenseクラスの定数として持たせることでこの無駄な処理を行わないようにできます。

class License
  Nothing = NullLicense.new
end

class NullLicense
  def name
    '免許なし'
  end
end

class Customer
  def initialize(license=nil)
    @license = license
  end

  def license
    @license || License::Nothing
  end
end

Rubyの場合はNil Objectパターンと呼ぶか?

Rubyにおいては、nilがnullみたいな扱いなので、Nil Objectパターンと呼ぶべきでしょうか?
Ruby on RailsにもNull Objectパターンはモジュールですが登場していて、NullRelationという名前がついています。このため、RubyにおいてもNull Objectパターンと呼んでよさそうです。
https://github.com/rails/rails/blob/e44927fea94fdf29757df1d2b14f3e1e4ed73e5c/activerecord/lib/active_record/null_relation.rb

Null Objectパターンのうれしさ

Null Objectパターンを使う利点は次の通りです

  • 値が無い場合のあるオブジェクトを利用する側で、値がない場合のことを考慮した処理を書かなくて良くなる
  • 値がない場合の処理がNull Objectにまとまる。

デメリットとして考慮する必要があるのは、次のような点です。

  • Null Objectを返す処理が追加されるので、単純なプロパティの返却ではなくなる。単にプロパティを返してほしいだけだったのにインスタンスが生成されていて、パフォーマンスのボトルネックになる、ということがあるかもしれない。
  • インターフェースを共有することになるので、Null Objectを作る対象のインターフェースが追加されたらNull Objectも追従する必要がある

Null Objectパターンは、非常に便利なのですが、オブジェクトの生成のところで少し工夫が必要になります。いろいろなところに分岐が散ってしまいそうなとき有効なパターンです。

Active RecordとNull Objectパターン

どんなときに使いたいか

アソシエーションを張っているレコードがない場合の処理を書きたいときに、Null Objectパターンを使えたら便利です。

実装

class Customer < ApllicationRecord
  has_one :actual_license, class_name: 'License'

  def license
    actual_license || NullLicense.new
  end
end

class License < ApplicationRecord
  belongs_to :customer
end

class NullLicense
end

このようにすれば、アソシエーションしているLicenseレコードが無い場合はNullLILicenseを渡すことができます。実際のlicenseとの関連はCustomer#actual_licenseを呼び出すことになります。

実装のポイント

  • has_oneのclass_nameオプションを使ってLicenseを取得するメソッドの名前を変更する
  • licenseメソッドのゲッタを作ることでNullLicenseのインスタンスを返せるようにする

困りそうなポイント

NullLicenseのほうに値の無い処理が書けるようになりましたが、その代わり、構造上のややこしさを生み出しました。

  • ライセンスのない場合の処理を外部に書きたいとき、Customer#license.nil?のような判定をすると意図していない結果になる(NullLicenseが取得できるため)
  • licenseかlicense_actualかを呼び出し側で考慮しないと行けない部分が出てくるかもしれない

といったところです。2点目の回避策としては、NullLicenseの方にLicenseインスタンスが外から呼び出されるメソッドを実装してしまうという方法があります。例えばLicense#destroyが呼ばれる場合、このようにしてダミーのメソッドを作ります。

class NullLicense
  def destroy
  # 何もしない
  end
end

この方法は、NullLicenseの方に値がないときの処理がまとまる利点がありますが、構造の把握をややこしくしてしまうという欠点もあります。

Active RecordでNull Objectパターンはややこしいコードを生んでしまうかも

Null Objectパターンを使うためにアソシエーションの張り方を歪めているので、新たな問題を生んでしまう可能性があります。

このあたりのデメリットを許容した上で、値がないときのややこしい処理がビューやコントローラーに散ってしまうことの対策として使うのがよさそうです。

References

https://vain0x.github.io/blog/2016-08-18/null-object-patterns/
https://qiita.com/kasei-san/items/af10a948c34c317e7380

Discussion

クリーム大佐クリーム大佐

RubyでNull Objectパターンを利用する
CutomerクラスとLicenseクラスを使って、

Cutomer -> Customer