WPF に対する非依存性を促進する
たとえば、Windows Form で作成した既存のアプリケーションを、新たに WPF アプリケーションとして再構築するときに「再利用可能なモジュールだけが整理整頓されていたら」どんなに楽でしょうか。私たち「タマゴ倶楽部」のモットーにも「楽をするための努力なら惜しみません」を掲げているだけに、ここはアピールしておきたいことです。
モジュール TetrisContext.py は、WPF に依存するコードの断片を隠蔽するとともに、相互に独立したモジュールの分割統治を促進します。すると、要求仕様の変更に伴って、WPF に代わる新たなフレームワークを導入するときに、既存のアプリケーションの再利用性が向上します。
from System.Windows.Shapes import Polygon class XPolygon: def __init__(self, Points=None, **args): self.value = Polygon( HorizontalAlignment=HorizontalAlignment.Center, VerticalAlignment=VerticalAlignment.Center, Points=Points.value, **args)
Smalltalk 版(第1版)と違って、Python 版(第3版)はやや冗長な表現になってしまいがちです。なぜなら、Smalltalk ではオブジェクトに最適な制御構造を自由に定義できますが、Python ではコンパイラーに組み込みの制御構造に課された制約の下で、コードの断片を記述せざるを得ないからです。
def __iter__(self): for e in self.value.Points: yield e
Python では、組み込みの制御構造である for を使って、オブジェクトに最適な制御構造を実現する必要があります。ここでは、多角形を構成する各頂点の座標を順にアクセスする方法を提供します。すると、次に示すように、
class Omino(object): def shift(self, n=0): ... def _points(s): return XPointCollection([XPoint(e.X + n*W, e.Y) for e in s]) for s in self.shape: s.update(_points, s) ...
簡潔で見通しの良いコードを記述できるようになります。インスタンス属性 self.shape は、テトリミノを構成する4つの6角形を管理します。ここでは、for に続く制御変数 s が、6角形を構成する各頂点の座標を順に参照します。ここで注目して欲しいのは、各頂点をどのようにして参照するか(how)は知る必要がなく、それを使って何をしたいか(what)「テトリミノを左右にシフトするために、各頂点の座標を更新 update したい」を理解できればいいということです。
リファクタリング:Python におけるクロージャーの隘路
Python では、メソッド関数を使って(いくつかの制約はあるものの)クロージャーを記述できます。そこで、Smalltalk 版には及ばないものの、汎用性を損なわず簡潔な表現が可能となるように、Python 版でのリファクタリングを実践します。これを機に、リファクタリング前のコードの断片 《付録》Tetrimino3.py - 続・ひよ子のきもち との違いをつぶさに観察するのも一興です。
class Omino(object): def shift(self, n=0): ... def _points(s): return XPointCollection([XPoint(e.X + n*W, e.Y) for e in s]) for s in self.shape: s.update(_points, s) ...
メソッド _points では、クロージャーとしての特徴を活かして、テトリミノを構成する4つの多角形の各頂点の(移動後の)座標を与えます。引数 s の値は、実行時に確定します。そして、それは System.Windows.Shapes.Polygon の特徴を反映することになります。つまり、WPF に依存するコードの断片を、実行時に注入します。
ここで着目して欲しいのは、それを実行する状況(where)には依存するものの、それを実現する方法(how)には依存しないことです。これによって、アプリケーションに固有の情報と、アーキテクチャーに固有の情報とを、分割統治するのが容易になります。つまり、要求仕様の変更に際して、その影響をクロージャーの中に封じ込められます。
class Omino(object): def rotate(self, sign): ... def _points(n): return self.matrix1[self._rotate1(n)] for e, n in zip(self.shape[self.offset:], self.mino1): e.update(_points, n) ... class PiOmino(SigOmino): def rotate(self, sign): super(PiOmino, self).rotate(sign) def _points(n): return self.matrix2[self._rotate2(n)] for e, n in zip(self.shape[len(self.mino1)+1:], self.mino2): e.update(_points, n)
テトリミノの回転についても、同様です。テトリミノを構成する4つの多角形の各頂点の(移動後の)座標を与えるとともに、引数 n の値は、実行時に確定します。そして、それは 10 種類のテトリミノの形状 mino1/mino2 を反映することになります。つまり、ゲームに依存するコードの断片を、実行時に注入します。
class XPolygon: def update(self, points, *args): self.value.Points = points(*args).value
ここで着目して欲しいのは、それを実行する状況(where)を知る必要がなく、それを実現する方法(how)だけを知っていることです。これによって、アプリケーションに固有の情報(クロージャー)points(*args) には依存しない、アーキテクチャーに固有の情報(プロパティー).Points だけを使って、コードを記述できるようになります。つまり、要求仕様の変更に際して、その影響を受ける恐れがなくなります。
《付録》Tetrimino4.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 Omino(object): _mat1 = [ ( 1, -2), ( 2, 0), ( 1, 2), (-1, 2), (-2, 0), (-1, -2), ] _mat2 = [ ( 0, -4), ( 2, -4), ( 3, -2), ( 4, 0), ( 3, 2), ( 2, 4), ( 0, 4), (-2, 4), (-3, 2), (-4, 0), (-3, -2), (-2, -4), ] _strokeColor = Black def __init__(self, x, y, phase=0, color=None, mino1=, offset=1, ): self.x = x self.y = y self.phase = phase self.color = color self.mino1 = mino1 self.offset = offset self.matrix1 = self._matrix(x, y, self._mat1) self.shape = self._shape (x, y, self.phase) # -------------------- def _matrix(self, x, y, matrix): M = 2; Ox = HexStone._dx; W = HexStone._width ; Oy = HexStone._dy; H = HexStone._height; X = Ox + W*x; Y = Oy + H*y; self.pivot = self._spot(X, Y, M) return [self._spot(Ox + W*(x+ix), Oy + H*(y+iy), M) for ix, iy in matrix] def _spot(self, x, y, m): return XPointCollection([XPoint(x + dx*m, y + dy*m) for dx, dy in HexStone(x, y, False).vertices]) # -------------------- def _shape(self, x, y, phase): self.phase = self._rotate1(phase) return [XPolygon( Stroke=self._strokeColor, Fill=self.color, Points=self.matrix1[self._rotate1(n)] ) for n in self.mino1] def _rotate1(self, n, sign=1): return (n*sign + self.phase) % len(self._mat1) # -------------------- def rotateClockwise(self): # clockwise self.rotate(1) def counterClockwise(self): # counter-clockwise self.rotate(-1) def rotate(self, sign): self.phase = self._rotate1(1, sign) def _points(n): return self.matrix1[self._rotate1(n)] for e, n in zip(self.shape[self.offset:], self.mino1): e.update(_points, n) self._update() # -------------------- def shiftRight(self): # shift-right self.shift(1) def shiftLeft(self): # shift-left self.shift(-1) def shift(self, n=0): self.x += n self.matrix1 = self._matrix(self.x, self.y, self._mat1) W = HexStone._width def _points(s): return XPointCollection([XPoint(e.X + n*W, e.Y) for e in s]) for s in self.shape: s.update(_points, s) self._update() # -------------------- def _update(self): s = self.spots() if self.any([e in self.tray.leftEdge for e in s]): self.shiftRight() # left -> right if self.any([e in self.tray.rightEdge for e in s]): self.shiftLeft() # right -> left def spots(self): return [self._sPoint1(self.x, self.y, e) for e in self.mino1] def _sPoint1(self, x, y, e): dx, dy = self._mat1[self._rotate1(e)] return x+dx, y+dy def all(self, s): return False not in s def any(self, s): return True in s ## -------------------- class SigOmino(Omino): def _shape(self, x, y, phase): return [XPolygon( Stroke=self._strokeColor, Fill=self.color, Points=self.pivot, )] + super(SigOmino, self)._shape(x, y, phase) def spots(self): return [(self.x, self.y)] + super(SigOmino, self).spots() ## -------------------- class PiOmino(SigOmino): def __init__(self, x, y, phase=0, color=None, mino1=, offset=1, mino2=None, ): self.mino2 = mino2 self.matrix2 = self._matrix(x, y, self._mat2) super(PiOmino, self).__init__(x, y, phase, color, mino1, offset) def _shape(self, x, y, phase): return super(PiOmino, self)._shape(x, y, phase) + [ XPolygon( Stroke=self._strokeColor, Fill=self.color, Points=self.matrix2[self._rotate2(n)] ) for n in self.mino2] def _rotate2(self, n, sign=1): return (n*sign + self.phase*2) % len(self._mat2) # -------------------- def rotate(self, sign): super(PiOmino, self).rotate(sign) def _points(n): return self.matrix2[self._rotate2(n)] for e, n in zip(self.shape[len(self.mino1)+1:], self.mino2): e.update(_points, n) # -------------------- def shift(self, n=0): super(PiOmino, self).shift(n) self.matrix2 = self._matrix(self.x, self.y, self._mat2) # -------------------- def spots(self): s = super(PiOmino, self).spots() return s + [self._sPoint2(self.x, self.y, e) for e in self.mino2] def _sPoint2(self, x, y, e): dx, dy = self._mat2[self._rotate2(e)] return x+dx, y+dy ## -------------------- class TetriminoC(Omino): def __init__(self, x, y, phase=0): super(self.__class__, self).__init__(x, y, phase, mino1=(0, 1, 2, 3), color=Brown, offset=0, ) ## -------------------- class TetriminoO(SigOmino): def __init__(self, x, y, phase=0): super(self.__class__, self).__init__(x, y, phase, mino1=(0, 1, 2), color=Yellow, ) class TetriminoP(SigOmino): def __init__(self, x, y, phase=0): super(self.__class__, self).__init__(x, y, phase, mino1=(0, 1, 3), color=Magenta, ) class TetriminoB(SigOmino): def __init__(self, x, y, phase=0): super(self.__class__, self).__init__(x, y, phase, mino1=(0, 2, 3), color=Purple, ) class TetriminoY(SigOmino): def __init__(self, x, y, phase=0): super(self.__class__, self).__init__(x, y, phase, mino1=(0, 2, 4), color=Gray, ) ## -------------------- class TetriminoZ(PiOmino): def __init__(self, x, y, phase=0): super(self.__class__, self).__init__(x, y, phase, mino1=(0, 2), mino2=(0,), color=Red, ) class TetriminoL(PiOmino): def __init__(self, x, y, phase=0): super(self.__class__, self).__init__(x, y, phase, mino1=(0, 3), mino2=(0,), color=Orange, ) class TetriminoI(PiOmino): def __init__(self, x, y, phase=0): super(self.__class__, self).__init__(x, y, phase, mino1=(0, 3), mino2=(1,), color=Cyan, ) class TetriminoJ(PiOmino): def __init__(self, x, y, phase=0): super(self.__class__, self).__init__(x, y, phase, mino1=(0, 3), mino2=(2,), color=Blue, ) class TetriminoS(PiOmino): def __init__(self, x, y, phase=0): super(self.__class__, self).__init__(x, y, phase, mino1=(0, 4), mino2=(2,), color=Lime, ) ## --------------------