Null Object
1. 目的
処理が何も実行されないことを明示的に表現したい
- ↓のような
type
の値に応じてビジネスロジックを切り替えるStrategy
パターン風のプログラムを仮定する
- プログラムの全体はAppendix-1を参照
class Program:
def __init__(self, type):
self._logic = Factory.create(type)
def execute(self):
return self._logic.execute()
if __name__=='__main__':
for i in range(5):
p = Program(i)
result = p.execute()
print("{}:{}".format(i, result))
- この時、
type
の値によってビジネスロジックを返却するFactory
が以下のような実装の場合、未定義のtype
が渡された時、例外が発生してしまう(当然だが)
class Factory:
@staticmethod
def create(self, type):
if type == 1: return Logic01()
if type == 2: return Logic02()
raise ValueError('Error:Unknown Type')
- 普通はそれでも構わないが、ここでは「未定義の
type
が渡された場合でも処理を継続したい」という要件があると仮定する
- そのような要件がある時、エラーを発生させず、かつ何も処理が行われない(未定義なので)ことを保証したい
2. 課題
想定外のパラメータを与えられてもエラーを起こさず処理を継続したい
- 未定義のパラメータが渡されてもエラーとして処理を中断したくない
- 未定義のパラメータが渡された場合に行うべき具体的な処理はない(デフォルトの処理があるわけではない)
3. 解決策
何もしないダミーオブジェクト(Null Object)を導入する
-
Null Object
とは通常のビジネスロジックと同じインターフェイスを持つ「何もしない」ダミー実装である
- 具体例は以下の通り
class Logic(metaclass=ABCMeta):
@abstractmethod
def execute(self):
pass
class Logic01(Logic):
def execute(self):
# 何か複雑なロジックがあると考えること
...
return "わーい"
class NullLogic(Logic):
def execute(self):
# 具体的なロジックは何も無い
return "..."
-
Logic
はビジネスロジックのインターフェイス
-
Logic01
は具体的な処理を行うビジネスロジックの実装
-
NullLogic
がNull Object
-
NullLogic
は具体的な処理を一切行わないが、Logic
と同じインターフェイスを持つ為、Program
に渡すことができる
4. メリット
エラーは起きず、余計な副作用も発生しない
- 未定義のパラメータが渡された場合に「何もしない」ダミー実装を返却し、それを実行することで、「エラーを起こさず、何も処理されない(副作用が発生しない)」ことを保証できる
利用側に特別な処理を強要する必要が無くなる
-
Null Object
を用いず、利用側で代替の処理をすることも可能ではある
- 以下のサンプルのように、
Program
側でFactory
の例外をキャッチし、ダミー処理を実行するという方法もあるが、可読性や保守性の観点から好ましい実装とは言えない
-
Null Object
は利用側に余計な仕事を押し付けない点で優れている
class Program:
def __init__(self, type):
try:
self._logic = Factory.create(type) # 未定義のtypeを渡すと例外が発生されると仮定
except:
self._logic = None
def execute(self):
if self._logic is None:
return "..."
else:
return self._logic.execute()
if __name__=='__main__':
for i in range(5):
p = Program(i)
result = p.execute()
print("{}:{}".format(i, result))
5. デメリット
Null Objectを知らないと何をしているのか理解できない
-
Null Object
はその特性として「何もしない」のでNull Object
の概念を知らないエンジニアから見ると本当に意味不明のクラスでしかない
- あえて「何もしない」のだという設計意図を共有する必要がある
6. 注意事項
使いどころが難しい
- WEBアプリケーションでは「処理をする必要はないが、通常の処理が完了したように見せかけたい」ニーズはあまりない(むしろ積極的にエラーを返した方がよい)
- ゲームやクライアントアプリの界隈ではよく使われているようだ
- ダミー実装を利用するという意味ではユニットテスト時に「何もしない」モックを作ることがあるが、それも
Null Object
の利用の一例と言える
参考資料
Appendix-1 プログラム全体
-
Null Object
を適用したプログラムのサンプルを以下に示す
class Program:
def __init__(self, type):
self._logic = Factory.create(type)
def execute(self):
return self._logic.execute()
class Logic(metaclass=ABCMeta):
@abstractmethod
def execute(self):
pass
class Logic01(Logic):
def execute(self):
return "わーい"
class Logic02(Logic):
def execute(self):
return "うぇーい"
class NullLogic(Logic):
def execute(self):
return "..."
class Factory:
@staticmethod
def create(self, type):
if type == 1: return Logic01()
if type == 2: return Logic02()
return NullLogic() # いずれの条件にもマッチしない場合はNullLogicを返却する
if __name__=='__main__':
for i in range(5):
p = Program(i)
result = p.execute()
print("{}:{}".format(i, result))
1:わーい
2:うぇーい
3:...
4:...
5:...