Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
41
Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

Organization

NullObjectパターン

いきなりGOF外のデザインパターン
けど、よく使いそうな感じだったのでメモ
プログラムデザインのためのパターン言語 が初出

どんなパータン?

よくあるこんな書き方

obj.method() if obj

あるオブジェクトが nil でなければ、メソッドを呼び出す
こういうパターンが頻出する場合、obj に nil の代わりに何もしないメソッドを持つオブジェクト を格納しておく

インターフェースだけ持って、何もしないオブジェクトを用意することで、
そのオブジェクトを使う側は、オブジェクトが生成されなかった場合を気にする必要がなくなる
→ オブジェクトの状態を意識する必要が無い
→ 結合度が下がる

実装例

Factoryパターンとの組み合わせで、インスタンスの生成が行えない場合、NullObject を返す

module Animal
  class Cat
    def sound
      puts 'meow'
    end
  end

  class Dog
    def sound
      puts 'wow'
    end
  end

  class NilAnimal # NullObject
    def sound
    end
  end

  def self.create(type)
    Animal.const_get(type.to_s.capitalize).new rescue NilAnimal.new
  end
end

[:cat, :dog, :llama].map do |type|
  Animal.create(type)
end.each(&:sound)

これにより、Animal.create を使う側は、Animal.create が nil を返す場合を意識する必要がなくなる

  • 今回ならば、each(&:sound) の所を、each{|animal| animal.sound if animal } と書かなくて済む

リファクタリング案

NullObject は、複数生成する必要はないので、Singleton にする場合が多い

require 'Singleton'

module Animal
...
  class NilAnimal
    include Singleton
    def sound
    end
  end

  def self.create(type)
    Animal.const_get(type.to_s.capitalize).new rescue NilAnimal.instance
  end
end

リファクタリング案2

個人的には、汎用の NilObject class を1つ作っちゃっても良いと思ってる

  • Animal class に無いメソッドを呼ばれるのが困る場合は、別途 Animal::NilAnimal を作るべき
require 'Singleton'

class NilObject < BasicObject
  include ::Singleton
  def method_missing(*args) # どんなメソッドが呼ばれても、nil を返す
  end

  def respond_to_missing?(*args)
    true
  end
end

module Animal
  class Cat
    def sound
      puts 'meow'
    end
  end

  class Dog
    def sound
      puts 'wow'
    end
  end

  def self.create(type)
    Animal.const_get(type.to_s.capitalize).new rescue NilObject.instance
  end
end

[:cat, :dog, :llama].map do |type|
  Animal.create(type)
end.each(&:sound)

Rails4 以降なら Object#try が使える

require "rubygems"
require "active_support/core_ext"

module Animal
  class Cat
    def sound
      puts 'meow'
    end
  end

  class Dog
    def sound
      puts 'wow'
    end
  end

  def self.create(type)
    Animal.const_get(type.to_s.capitalize).new rescue nil
  end
end

[:cat, :dog, :llama].map do |type|
  Animal.create(type).try(:sound)
end

Object#try は、第一引数を public_send して、結果が取れればその結果を、
NoMethodError ならば、nil を返すメソッド
ただし、Rails 3.x までは、NoMethodError を返すので注意

参考

基本的に使へるなら使ふべきパターン。特に、「if 文で null かどうか判定して null でなければメソッドを呼び出す」の様なパターンがいくつもある場合は設計が怪しいので null object パターンを真剣に検討した方が良い。
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
41
Help us understand the problem. What are the problem?