リファクタリング:switch 文の隘路
switch 文が抱えるハードコーディングの問題点を回避するとともに、動的スキーマを適用して、実行時にオブジェクトの動作を規定する方法を紹介します。
すでに、矢印キーを使って、テトリミノの回転/シフトができます。これに加えて、space キーを押すと、テトリミノを落下できるようにします。
伝統的な構造化プログラミングの手法では、このような問題解決にあたって switch 文による条件分岐を導入します。しかし、ここで問題となるのは、新たな機能を追加したいときに、switch を伴うコードの断片を更新する必要が生じることです。そのため、要求仕様の変更が必要になることから、そのモジュールを永遠に閉じることができません。これは問題です。
class Tetris: def __init__(self, client): self.client = client for item, mino in self.client.testCase: tray = Tray() mino.tray = tray mino.dispatch = self._cases(mino) self.client.addItem(item, mino) self._addTray(tray, item) self._addMino(mino, item)
テトリミノのインスタンス属性 .dispatch を介して、特定のキーを操作したときの処理を「実行時に拡張できる」仕組みを提供します。
def _cases(self, mino): return { Key_Right: mino.shiftRight, Key_Left : mino.shiftLeft , Key_Up : mino.counterClockwise, Key_Down : mino.rotateClockwise , } def keyDown(self, sender, e): KeyHolder(e.Key).switch(self.client.cases()) self.client.update()
このように、既存のモジュールでは、矢印キーを使ってテトリミノの回転/シフトができますが、この部分はなにも変更を加えません。その代わり、テストケースには、新たな機能だけを追加します。
class ExWindow(Window): def cases(self): return self.mino.dispatch def _attach(self, e, dispatch): key = Key.Space if key in dispatch: return if e == "C": dispatch[key] = self.downC; print "**C**" if e == "O": dispatch[key] = self.downO; print "**O**" def downC(self): print "<<< C" def downO(self): print "<<< O"
すると、2種類のテトリミノ(C型/O型)のテストケースでは(新たに)space キーには応答しますが、その他のテトリミノは(従来のまま)応答しません。つまり、既存のモジュールは(閉じたまま Closed)変更せずに、新たな機能だけを付加できます。switch を伴うコードの断片に「外部から機能を注入した」ことになるのです。
こうして、テトリミノを落下させるときに、異なるテストケースを用意して、それぞれを独立してその動作を検証できるようになります。そのため、わずかな機能の違いだけを実現するために、異なるテストケースを反映させた個別のクラスを定義する必要もなくなります。
《付録》TetrisCenter.py
# -*- coding: utf-8 -*- #=============================================================================== # Copyright (C) 2000-2008, 小泉ひよ子とタマゴ倶楽部 # # Change History: Games # 1988/05, Smalltalk # 2004/09, Java # 2005/02, C# # 2005/03, Jython # History: WPF examples # 2008/01/25, IronPython 1.1.1 (download) # 2008/08/22, IronPython 1.1.2 (download) # 2008/03/16, ver.2.0, WPF # 2008/00/00, ver.2.1, IronPython 1.1.2 #=============================================================================== from TetrisContext import * from TetrisWorld import * ## -------------------- class Tetris: def __init__(self, client): self.client = client for item, mino in self.client.testCase: tray = Tray() mino.tray = tray mino.dispatch = self._cases(mino) self.client.addItem(item, mino) self._addTray(tray, item) self._addMino(mino, item) def _cases(self, mino): return { Key_Right: mino.shiftRight, Key_Left : mino.shiftLeft , Key_Up : mino.counterClockwise, Key_Down : mino.rotateClockwise , } def _addTray(self, tray, item): targets = "leftEdge", "rightEdge", "bottomEnd", "tiles", for e in targets: for stone in getattr(tray, e).values(): self.client.addShape(stone.shape, item) def _addMino(self, mino, item): for e in mino.shape: self.client.addShape(e, item) ## -------------------- def keyDown(self, sender, e): KeyHolder(e.Key).switch(self.client.cases()) self.client.update() ## -------------------- class KeyHolder: def __init__(self, value): self.value = value def switch(self, cases): for key, f in cases.items(): if key == self.value: f() ## --------------------
《付録》TetrisWorld.py
# -*- coding: utf-8 -*- #=============================================================================== # Copyright (C) 2000-2008, 小泉ひよ子とタマゴ倶楽部 # # Change History: Games # 1988/05, Smalltalk # 2004/09, Java # 2005/02, C# # 2005/03, Jython # Change History: WPF examples # 2008/01/25, IronPython 1.1.1 (download) # 2008/08/22, IronPython 1.1.2 (download) # 2008/03/16, ver.2.0, WPF # 2008/00/00, ver.2.1, IronPython 1.1.2 #=============================================================================== from hexagon import HexStone from TetrisContext import * ## -------------------- class Tray: M = 2; M2 = M*2; def __init__(self): self.tiles = self._tiles() self.leftEdge = self._leftEdge() self.rightEdge = self._rightEdge() self.bottomEnd = self._bottomEnd() def _tiles(self): m1, m2 = self.M, self.M2; s = {} for x, y in [(e*m1+2, 0) for e in range(5)]: for _ in range(5): s[x, y] = Tile(x, y); y += m2 for x, y in [(e*m1+3, 2) for e in range(4)]: for _ in range(4): s[x, y] = Tile(x, y); y += m2 return s def _leftEdge(self): return self._edge( 0) def _rightEdge(self): return self._edge(12) def _edge(self, x): m = self.M; s = {} for x, y in [(x-1, e*m) for e in range(9)]: for _ in range(3): s[x, y] = Edge(x, y); x += 1 return s def _bottomEnd(self): s = {} for x, y in [(e+2, 18) for e in range(9)]: s[x, y] = Bottom(x, y) return s ## -------------------- class Ostone(object): def __init__(self, x, y, strokeColor=None, fillColor=None): self.x = x self.y = y self.strokeColor = strokeColor self.fillColor = fillColor self.shape = self._shape() def _shape(self): M = 2; Ox = HexStone._dx; W = HexStone._width ; Oy = HexStone._dy; H = HexStone._height; X = Ox + W*self.x; Y = Oy + H*self.y; points = XPointCollection([XPoint(X + dx*M, Y + dy*M) for dx, dy in HexStone(X, Y, False).vertices]) return XPolygon( Stroke=self.strokeColor, Fill=self.fillColor, Points=points, ) class Tile(Ostone): def __init__(self, x, y): super(self.__class__, self).__init__(x, y, strokeColor=Gray, ) class Edge(Ostone): def __init__(self, x, y): super(self.__class__, self).__init__(x, y, strokeColor=LightGray, ) class Bottom(Ostone): def __init__(self, x, y): super(self.__class__, self).__init__(x, y, fillColor=LightGray, ) ## --------------------
状況設定
ここでは、switch 文が抱えるハードコーディングの問題点を解消するために、既存のモジュール 《付録》TetrisCenter.py - 続・ひよ子のきもち を閉じます。 そして、開放閉鎖原則〔OCL: Open-Closed Principle〕に沿って、このモジュールを(閉じたまま Closed)変更せずに、テストケース 《付録》exTetrimino4.py - 続・ひよ子のきもち を記述するときに、新たな機能を追加できるようにします。
祝・成人
本間りすちゃん、成人式おめでとう。-- タマゴ倶楽部スタッフ一同
まだにきび はたちすぎたら ふきでもの
あの〜まだ誕生日前なので未成年なんですけど。 by りす