Active RecordとNull Objectパターン
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パターンが有効に働くことがあります。
存在しないことを表現する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パターンと呼んでよさそうです。
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
Discussion
Cutomer -> Customer