Tbpgr Blog

Employee Experience Engineer tbpgr(てぃーびー) のブログ

Ruby | Collecting Inputs | Represent do-nothing cases as null objects

概要

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

出力

私は田中です
田中が歩いています
私は佐藤です
佐藤が歩いています