概要
Represent do-nothing cases as null objects
前提
Confident Rubyではメソッド内の処理を次のように分類しています。
・Collecting Inputs(引数チェック、変換など)
・Performing Work(主処理)
・Delivering Output(戻り値に関わる処理)
・Handling Failure(例外処理)
当記事は上記のうち、Collecting Inputsに関する話です。
詳細
状況
メソッドの入力の一つにnilがあるかもしれないケース。
nilの存在は、特別なケースの印です。
通常の動作の代わりに入力を無視し、何もしないケースを取り扱います。
例えばオプショナルのロガーを扱う場合。ロガーがnilなら何もロギングを行いません。
概要
特別なNullObjectでnilを置き換えます。
NullObjectは通常のcollaboratorと同じインターフェースを持ちます。
しかし、メッセージを送られても何もしません。
理由
何もしないオブジェクトはnilチェックを撲滅する。
また、外部システムのテスト・ドライラン・"quiet mode"・切断動作などのために
"nullify"な動作をさせることができる。
サンプルコード仕様
ドラクエのモンスターを表すクラスを作成します。
モンスターは「攻撃」か「逃げる」を振るまいとして持ちます。
ただし、「爆弾岩」と「メガザルロック」は様子をうかがっているため何もしてきません。
サンプルコード(その1)
NullObjectを利用しないケース
require 'pp' class Monster attr_reader :name def initialize(name) @name = name end def atack puts "#{@name}の攻撃" end def escape puts "#{@name}は逃げ出した" end end monsters = [Monster.new("スライム"), Monster.new("爆弾岩"), Monster.new("ドラキー"), Monster.new("メガザルロック")] monsters.each do |monster| unless ["爆弾岩", "メガザルロック"].include? monster.name monster.atack monster.escape end end
出力
スライムの攻撃 スライムは逃げ出した ドラキーの攻撃 ドラキーは逃げ出した
サンプルコード(その2)
NullObjectを利用するケース。
分岐がなくなりました。
require 'pp' class Monster attr_reader :name def initialize(name) @name = name end def atack puts "#{@name}の攻撃" end def escape puts "#{@name}は逃げ出した" end end class NotMoveMonster attr_reader :name def initialize(name) @name = name end def atack end def escape end end monsters = [Monster.new("スライム"), NotMoveMonster.new("爆弾岩"), Monster.new("ドラキー"), NotMoveMonster.new("メガザルロック")] monsters.each do |monster| monster.atack monster.escape end
出力
スライムの攻撃 スライムは逃げ出した ドラキーの攻撃 ドラキーは逃げ出した
汎用的なNullObjectの利用
method_missingを利用することで、汎用的なNullObjectを作成できます。
この際、基底クラスをBasicObjectにしておくことで余計なメソッドを保持せず
軽量なクラスを作成できます。
require 'pp' class NullObject < BasicObject def method_missing(*args) end def respond_to?(method_name) true end end class Person attr_reader :name def initialize(name) @name = name end def call_oneself "私は#{@name}です" end def walk "#{@name}が歩いています" end end def action(person = NullObject.new) puts person.call_oneself puts person.walk end tanaka = Person.new("田中") sato = Person.new("佐藤") action(tanaka) action(sato) action
出力
私は田中です 田中が歩いています 私は佐藤です 佐藤が歩いています