NullObjectパターン(PLoPD)

あるクラスと同じインターフェースを持つが何もしないクラスを用意するパターン。スペシャルケース(SpecialCase)パターンの一種。

たとえば、オークションサイトで買い手がいたら、買い手に通知をする処理を実装しているのだとする。

def notify_buyer_message(item, message)
  return if item.buyer.nil?

  item.buyer.notice(message)
end

買い手がいない場合、item.buyernilになるため、nullチェックが必要になる。以下のようにItemNullBuyerクラスを定義することでnullチェックが不要になる。

def notify_buyer_message(item, message)
  item.buyer.notice(message)
end

class Item
  def buyer
    @buyer ||= NullBuyer.new
  end
end

class Buyer
  def notice(message)
    # 通知処理...
  end
end

class NullBuyer < Buyer
  def notice(message)
    # 何もしない。必要なら成功時の戻り値を返す
  end
end

継承よりもコンポジション(委譲)でも書いたが、NullObjectパターンは脆いクラス構造だと考えている。基底クラスにメソッドが追加された場合、NullObjectクラスにもメソッドを追加しなければならないが、追加を忘れてもオーバーライドしていないだけなのでエラーにはならない。したがって、NullObjectクラスを作るのであれば、継承関係を持たない同じインターフェースを実装したクラスとして実装した方が良いと考えている。

もっといえば、可能ならNullObjectクラスを作らず、存在しないケースも含めた1つのクラスとしてしまった方がよい。

class Buyer
  # void_buyerフラグがtrueならNullObjectとして振る舞う
  def initialize(name, void_buyer: false)
    # ...
    @void_buyer = void_buyer
  end

  def notice(message)
    return if @void_buyer
    # 通知処理...
  end
end

フラグによらず、値によってNullObjectとして振る舞うべきか決めることもできる。