リファクタリング: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 文に象徴されるハードコーディングの問題点は、要求仕様の変更に伴って、コードの改変を余儀なくされることです。そのため、いつまでもそのモジュールを閉じることができません。そこで、この問題点を解消するとともに、開放閉鎖原則に沿って、モジュールを閉じたまま機能拡張できるアーキテクチャーを構築します。

状況設定

ここでは、switch 文が抱えるハードコーディングの問題点を解消するために、既存のモジュール 《付録》TetrisCenter.py - 続・ひよ子のきもち を閉じます。 そして、開放閉鎖原則〔OCL: Open-Closed Principle〕に沿って、このモジュールを(閉じたまま Closed)変更せずに、テストケース 《付録》exTetrimino4.py - 続・ひよ子のきもち を記述するときに、新たな機能を追加できるようにします。