スペシャルケース(SpecialCase)パターン(PofEAA)

参考書籍一覧(Amazon アソシエイトリンク)

SQLのNULLや浮動小数計算のNaNのような存在をサブクラスとして定義するパターン。

SQLのNULLはカラムのドメイン(定義域)から外れた値のようなものとして存在し、NaNも浮動小数点数から外れた値のようなものとして存在している。数学っぽくいえば、本来とりうる値の集合Sがあって、そこから外れた存在としてNULLやNaNがある(補集合の要素ともいえない存在)。オブジェクトにおいても、本来とりうるインスタンスの集合があって、そこから外れた存在もインスタンスとして扱いたい場合がある。

SQLのNULLのありがちな使われ方がわかりやすいので、これを例に使う。

NULLにはさまざまな意味がある。たとえば、以下の4つが考えられる。

(1)失われた値

(2)未知の値

(3)雑多な値

(4)適用不能な値

(中略)私がイースターエッグを探していて、その色を知りたがっている、という例を考えてみよう。穴の中に卵が落ちてしまっている場合、卵は失われてしまっている(1)——あとで取り戻せれば良いのだが。次に、卵がアルミフォイルでくるまれている場合、卵はあるのだが何色かはわからない(2)。さまざまな色が混合している卵の場合、その色は一位な値に決まりきらない雑多な値を持つ(3)。もし私が見つけたのが卵ではなくビリヤードの玉だった場合、その色は適用不能である(4)。

プログラマのためのSQL 第4版 p.68

会員特典に関するドメインロジックを持つクラスMembershipBenefitがあったとして、特典を受ける権利を剥奪されたユーザーや、未入金のため特典を適用して良いか不明なユーザー、特典が適用できない匿名ユーザーなど個別処理が必要なケースを考える。

通常ケース以外はMembershipBenefitのインスタンスを取得する時にnilを返して呼び出し元で特殊ケースに対処する方法も取れるが、呼び出し元のコードが複雑化するし複数箇所で利用されていれば、同じコードがばら撒かれることになる。MembershipBenefitの各メソッドで特殊ケースをサポートする条件分岐をつけることもできるが、特殊ケースの数が増えたり処理内容が複雑化すれば、手に負えない状況になる。

そのため、以下のようにMembershipBenefitを継承した特殊ケース用のクラスを作る方法をとる。

class MembershipBenefit
end

# 失われた状態の会員特典
class LostedMembershipBenefit < MembershipBenefit
end

# 未知状態の会員特典
class UnknownMembershipBenefit < MembershipBenefit
end

# 適用不能状態の会員特典
class NotApplicableMembershipBenefit < MembershipBenefit
end

ただ、継承を使っている以上、MembershipBenefitクラスに追加したメソッドをオーバーライドし忘れて不具合が起きる可能性がある。継承関係を持たずに同じインターフェースを実装するクラス群とした方が良いだろう。部分的に差し替える必要がある場合も、継承せずに差し替える部分をStrategyパターンにして、Strategyクラスをスペシャルケースで実装する方法で回避できる。継承よりもコンポジション(委譲)も参照。