デザインパターン:Null Object
GoFのカタログにはない、古典的なパターンの一つがNull Objectです。
class Stone(GameItem):
def __nonzero__(self): return True
class NullStone(Stone):
def __nonzero__(self): return False
クラスStoneは、オセロゲームの石を実現します。これには、面の状態(白/黒)を表すインスタンス属性stateが必要です。クラスNullStoneは、ゲーム開始するときに、眼に見えない石が置いてあるものと見なします。先の連載で紹介した事例(パズルゲーム/ライフゲーム)では、固有のメソッド定義を持つ、唯一の実体(soleinstance)として実現しました。ここでは、多数のインスタンスが必要なので、Stoneの子孫NullStoneとして実現します。
先の連載で紹介したNull Objectは、盤面に石が置かれていない状態を表現するのに好都合で、これを特殊な石と見なします。つまり、白黒どちらの領地でもないマス目には「眼に見えない」特殊な石が存在します。これは、すべてのオブジェクトを平等に扱いつつ、その個性を尊重するという管理手法に適います。Null Objectに関する広範な論議は、Smalltalkが参考になります。
class Stone(GameItem):
def paintItem(self, g):
width = self.width(g)
height = self.height(g)
x = self.x * width
y = self.y * height
color = (Color.black, Color.white)[self.state]
g.color = color
g.fillOval(x, y, width, height)
g.color = Color.black
g.drawOval(x, y, width, height)
class NullStone(Stone):
def paintItem(self, g): pass
補助関数paintItemは、石を描きます。クラスStoneでは、その状態によって、黒 Color.green/白 Color.whiteの石を描きます。クラスNullStoneでは、何もする必要がないので、その本体は空passとなります。NullStoneを導入すると、Noneかどうかを判定する条件式が不要になるので、コードの見通しがよくなります。
sole instanceとしてのNull Object
nullStone = NullStone(None, None, None)
同じ見えない石でも、どれとも異なるインスタンスnullStoneが、盤面の外に存在するものと見なします。このとき、インスタンス属性stateには、表裏(黒/白)どちらの状態でもない値Noneを設定すると共に、その座標x/yも未定義とします。
nullStoneの実体(sole instance)は一つだけ存在しますが、あたかも盤面を取り囲むかのように図示してあります。次にビューを
次に、ビューに着手します。これは8×8のマス目を持つパネルとして実現します。
class GameBoardPanel(JPanel):
def __init__(self):
JPanel.__init__(self,
mouseClicked=self.this_mouseClicked)
self.initialize()
def initialize(self):
self.items = []
def detect(self, x, y):
obj = self.nullObject()
for e in self.items:
if e.isExist(x, y): obj = e; break
return obj
def paintItems(self, g):
for e in self.items:
e.paint(g)
抽象クラスGameBoardPanelは、共通するプロトコルを規定します。ここで着目して欲しいのは、具象クラスを規定していないことです。組み込み型listや、インターフェイスShapeには依存しても、特定のクラスには依存しない「テンプレート」として機能します。すると、再利用性が高く、仕様の変更にも柔軟に対処できます。
メソッド__init__は、ビュー(視覚部品)の特性を規定します。キーワード引数mouseClicked=には、イベントハンドラーとして、関数オブジェクトthis_mouseClickedを指定します。
メソッドinitializeは、石をパネルに配置します。インスタンス属性itemsは、パネルに配置するすべての実体(インターフェイスShapeの規定を満たす任意のインスタンス)を保持します。
itemsだけを初期設定します。すると、仕様変更の影響を受けず、OCPにも準拠していることから、このモジュールを閉じる条件が整います。必要なら、その子孫クラスでinitializeを再定義しますが、このときに閉じた箇所をモジュールとして再利用するのが、約束事となっています。OCPに関する広範な論議は、Eiffelが参考になります。 メソッドdetectは、指定した座標x/yを持つ実体を検出します。ただし、実際の処理は、インターフェイスShapeの規定を満たすインスタンスeに委ねます。最後に、その条件を満たす実体objをリターン値とします。
メソッドpaintItemsは、バネルに配置したすべての実体を描画します。

class GameBoardPanel(JPanel):
def nullObject(self):
raise NotImplementedError, "def nullObject(self)"
class OthelloPanel(GameBoardPanel):
def nullObject(self):
return nullStone
Null Objectパターンを活用するには、その対象となるオブジェクトが必要です。これを何にするかは、プロトコルに従って、メソッドnullObjectで指定します。クラスOthelloPanelでは、盤面の外を取り囲む特殊な石nullStoneをリターン値とします。
