岬めぐり:コードを巡る旅の縁(よすが)に
同じ景色を眺めても、思い入れがあるのとないのとでは、その感動も違ってきます。そこで、漠然とコードを眺めるのではなく、構造化プログラミング(SP)とオブジェクト指向プログラミング(OOP)との違いを示しながら、その相互理解を深めます。
そこにコマが移動するのは

移動できるコマは、空いたマス目と隣接するものだけです。配列を用いた伝統的な手法では、これを配列の値を入れ換える操作として実現します。そのとき、主役を演じるのは配列で、値は脇役にすぎません。OOPでは、これをオブジェクト間のメッセージ送受信としてモデル化します。そこでは、コマ(オブジェクト)が主役を演じます。

2つのオブジェクトが「座標を交換swapしませんか」というメッセージ(what)をやり取りする様子を、メソッド(how)として実現することを目指します。つまり、Tileインスタンス(コマ)とNull(空いたマス目)とが「座標を交換したい」とする意思の伝達と考えます。そこでは、空いたマス目を特殊なコマと見なして、既存のコードを再利用します。また、目的(what)と手段(how)とを明確に区別する戦略は、関心の分離(separation of concerns)の趣旨にも適います。
class Tile(Shape):
def swap(self):
self.x, Null.x = Null.x, self.x
self.y, Null.y = Null.y, self.y
メソッドswapでは、タプルを使って値を交換します。演算子=の両辺では、左辺に列挙した変数が順に、右辺の対応する値を束縛(結合)します。すると、式a,b = b,aによって、変数a/bが参照する値を交換する操作を、簡潔に表現できます。
そこに条件式が介在するのは
カプセル化によって、インスタンス属性x/yと、メソッド操作moveをひとまとめにして、公開インターフェースを明確に規定すると、不適切な操作は適用できません。そのプロトコルに従うなら、例外が発生する余地すらなくなります。
先のJava版のコードと、次のコードの断片にある条件式とを比較してください。
class Tile(Shape): def move(self): if self.x-1 == Null.x && self.y == Null.y: self.swap() return True if self.x+1 == Null.x && self.y == Null.y: self.swap() return True if self.x == Null.x && self.y-1 == Null.y: self.swap() return True if self.x == Null.x && self.y+1 == Null.y: self.swap() return True return False
同じ条件式でも、次のように、
if (x>0 && board[y][x-1] == n) {
(配列に起因する)例外を避けるための便宜的な手段(how)と、次のように、
if self.x-1 == Null.x && self.y == Null.y:
(ゲームの規則)要求仕様に記載した目的(what)とでは、その趣旨が異なります。what/howを分離すると、コードの見通しがよくなるだけでなく、例外も発生しないので、一石二鳥です。
リファクタリング:ピタゴラスの定理を使って
隣接するコマの位置関係を判定するのに、ピタゴラス(三平方)の定理を用いると、条件判定を整理整頓(リファクタリング)して、見通しの良いコードを記述できます。
class Tile(Shape): def move(self): b = self.isInContactWith(Null) if b: self.swap() return b def isInContactWith(t1, t2): dx = t1.x - t2.x dy = t1.y - t2.y return dx*dx + dy*dy == 1
メソッドmoveを実現するときには、4つの条件判定に共通の論理構造を抽出した、isInContactWithを再利用します。三平方の定理を使って、2つのコマの距離が1と等しいかどうか(隣接するか)を判定します。条件を判定するだけなので、平方根を求める必要はありません。自乗和を求めれば十分です。コードの見通しがよくなると、バグを発見するのも容易になります。
