伝統的な手法による問題解決:2次元配列
伝統的な手法による問題解決では、2次元配列を使うのが定石です。

ボードに配置したコマを、2次元配列で表現します。配列要素には、コマを識別する整数値を代入します。空いたマス目は-1で識別します。移動できるコマは、空いたマス目と隣接(上下左右)するものだけです。コマを移動するときには、移動元の整数値を移動先の配列要素に代入すると共に、移動元には値-1を代入します。
Javaで記述したコードの断片は、次のようになるでしょう。
void move(int n) { if (x>0 && board[y][x-1] == n) { board[y][x-1] == -1; board[y][x] == n; } if (x<3 && board[y][x+1] == n) { board[y][x+1] == -1; board[y][x] == n; } if (y>0 && board[y-1][x] == n) { board[y-1][x] == -1; board[y][x] == n; } if (y<3 && board[y+1][x] == n) { board[y+1][x] == -1; board[y][x] == n; } }
ここで着目して欲しいのは、添字が範囲内にあるかを判定する条件式です。ボードの左端では、条件式x>0を満たすかどうかで判定します。同様に、右端ではx<3で判定します。上端/下端も同様です。その範囲外で値を代入しようとすると、実行時に例外を生成します。そのため、伝統的な手法では、この条件式は必要悪とされてきました。
諸悪の根源は何処に
このプログラムは、意図したように動作します。動かないプログラムより、動くプログラムが望ましいのは確かです。しかし、問題がないわけではありません。例えば、「マス目の数を増やしたい」という仕様変更があったとします。すると、すべての条件式を見直す必要があります。条件式x>0およびx<3などに伴う「マジックナンバー」と呼ばれる定数を排除していたら、仕様変更にも柔軟に対処できたかもしれません。しかし、例外が発生するのを防ぐためだけに条件式が必要になるという不条理には、納得がいきません。なぜなら、開発者の都合に、利用者が左右されたくないからです。
この不条理を避けて、見通しの良いコードを記述するにはどうすればよいでしょう。諸悪の根源は何でしょうか。伝統的な手法では、ゲーム盤を表現するのに2次元配列を利用しました。「配列」さえ利用しなければ、この問題を回避できるかもしれません。もしかすると、自分が招いた問題を、自分で解決する羽目に陥っていたのでしょうか。これは、確かめてみるだけの価値がありそうです。
愛が止まらない:隘路を切り開く
「その一口がブタになる」と分かっていても、甘いものにはつい手が伸びてしまう。頭では分かっていても、身体が言うことを聞きません。配列の「甘い」誘惑に負けそうになったら、この話を思い出してください。
まず、コマを単なる値ではなく、オブジェクトとして表現することから始めます。

class Tile(Shape): def __init__(self, x, y, value): self.x = x self.y = y self.value = value def __str__(self): return "%r(%d,%d)"%(`self.value`, self.x, self.y)
番号'5'が振られたコマは、x座標0、y座標2、整数値5を持つインスタンスと見なせます。ここで、コマの左上の隅を、その座標値とします。インスタンスに固有の情報を文字列として出力すると、次のようになります。
'5'(0,2)
この結果は、メソッド__str__で規定した文字列表現から得られます。
配列に値を保持させるのではなく、その対象を「オブジェクト」として実現します。番号を振られたコマは、単なる整数値ではありません。自分がどこに位置すべきかは、コマ自身で判断します。つまり、オブジェクト自身が「思考」するというわけです。
デザインパターン:Null Object
GoFのカタログにはない、古典的なパターンの一つがNull Objectです。
class Tile(Shape): def backgroundColor(self): return Color.white def drawValue(self, g, x, y): g.drawString(`self.value`, x+10, y+20)
コマには、2つのメソッドを定義します。メソッドbackgroundColorは、コマの背景色(白色 Color.white)を規定します。メソッドdrawValueは、コマを識別するために、整数値self.valueを座標x,yに表示します。
Null Objectは、空のマス目を表現するのに好都合で、これを特殊なコマと見なします。これは、すべてのオブジェクトを平等に扱いつつ、その個性を尊重するという管理手法に適います。Null Objectに関する広範な論議は、Smalltalkが参考になります。
インスタンスに固有のメソッド定義
N = PuzzlePanel.dim - 1; Null = Tile(N, N, 0) Null.backgroundColor = lambda: Color.lightGray Null.drawValue = lambda g, x, y: None
空のマス目Nullには、2つのメソッドを再定義します。メソッドbackgroundColorは、コマの背景色(灰色 Color.LightGray)を規定します。メソッドdrawValueは、何も表示する必要がないので、その本体は空Noneとなっています。すると、Nullは、他のコマと違う個性を発揮します。
class Tile(Shape): def paintBackground(self, g): width = self.width(g) height = self.height(g) x = self.x*width; y = self.y*height; g.color = self.backgroundColor() g.fillRect(x, y, width, height) def paintItem(self, g): width = self.width(g) height = self.height(g) x = self.x*width; y = self.y*height; g.color = Color.black g.drawRect(x, y, width, height) self.drawValue(g, x, y);
メソッドpaintBackgroundは、コマの背景を描きます。背景色を得るために、backgroundColorを再利用します。コマ(Tile インスタンス)なら白色が、空いたマス目(Null)なら灰色が得られます。
メソッドpaintItemは、コマの番号を描きます。番号を描くために、drawValueを再利用します。コマ(Tile インスタンス)なら数字を表示しますが、空いたマス目(Null)なら何も表示しません。
Nullを導入すると、Noneかどうかを判定する条件式が不要になるので、コードの見通しがよくなります。
