リファクタリング: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 を伴うコードの断片に「外部から機能を注入した」ことになるのです。
こうして、テトリミノを落下させるときに、異なるテストケースを用意して、それぞれを独立してその動作を検証できるようになります。そのため、わずかな機能の違いだけを実現するために、異なるテストケースを反映させた個別のクラスを定義する必要もなくなります。