はじめに
伝統的なスタイルを卒業して、OOPを習得するときに、越えなければならない壁の一つが配列です。2次元配列を扱った事例は多数ありますが、ともすると、配列が主役になり、その中身が脇役になる場面も少なくありません。そこで、発想を転換して主客逆転させると、新しい世界、オブジェクト指向の世界が開けてきます。
その効用は、冗長な条件判定や例外処理が不要になり、例外が発生するのを防ぐだけではありません。コードの見通しをよくし、バグの発見が容易になる、強力な処方箋となります。
今回は、第2回に続いて、伊藤が担当します。先の連載で紹介した2つのゲームから、共通するフレームワークを抽出して、それをもとにオセロゲームを作成します。前回の復習を兼ねて、初心に還ったつもりで解説しますので、よろしくお願いします。
対象読者
こんな症状を抱えているなら……。
- 配列を扱うたび、例外
ArrayIndexOutOfBoundsExceptionに悩まされる
オセロゲームはコミュニケーションツール
今回は、新しい話題を提供すると共に、先の連載の内容を再検討するために、新しい課題を紹介します。
オセロゲームの原型は、長谷川五郎さんが、戦後間もない1945年に発案したとされています。その名前の由来は、シェークスピア作の劇中で、白人の妻を持つ黒人将軍オセロが、緑の平原で勇敢に戦う波瀾万丈の物語にちなんだものだそうです。1972年、当時(株)ツクダの社長との出会いが、本格的な普及への扉を開きます。その後、日本オセロ連盟(1973年)、世界オセロ連盟(1976年)が設立され、世界中に多くの愛好家がいます。今日では、老若男女、人種を問わない、バリアフリーのコミュニケーションツール、脳細胞の活性化(脳トレ)ツールとしても注目されています。
オセロゲームのルール:要求仕様

ルールは簡単です。マス目の中央に白黒の石を2つずつ並べた状態から、ゲームを開始します。使用する石は、表裏が黒/白に塗り分けられています。先手(黒)後手(白)が、交互に石を置き続けます。相手の石を自分の石で挟むと、その場所が自分の領地となります。どこにも置けないときには順番をパスできますが、どこかに置ける限りパスはできません。盤面をすべて埋め尽くすか、双方がどこにも置けなくなると、ゲームは終了します。自分の領地にある石が多い方が勝者となります。典型的なマス目(8×8)の他に、六角形の形をしたものなど、いくつかの変種が存在します。
どうにも止まらない:隘路(あいろ)を切り開く
アプリケーションを作成するときに「どこから着手すべきか」という問いに、唯一の正解はありません。さまざまな方法論の中から、状況に応じて最適解を模索するものです。今回は、モデル側から作り始めます。このとき、モデル作りがどこまで進んだら、どこで止まるかが鍵となります。
まず、石を単なる値ではなく、オブジェクトとして表現することから始めます。

はじめに光(インターフェイス)ありき
モデルを規定するときに限らず、メッセージ(目的 what)とメソッド(手段 how)とを明確に区別する戦略は、関心の分離(separation of concerns)の趣旨にも適います。カプセル化によって、インスタンス属性とメソッド操作とを一元管理すると共に、公開インターフェイスを明確に規定するのは、情報隠蔽の原則にも適います。
class Shape:
def isExist(self, x, y):
raise NotImplementedError, "def isExist(self,x,y)"
def paint(self, g):
raise NotImplementedError, "def paint(self,g)"
def dim(self):
raise NotImplementedError, "def dim(self)"
クラスShapeは、インターフェイスの役割を担い、子孫クラスに共通するプロトコルを規定します。これらのメソッドは、子孫クラスで『必須として』再定義する必要があり、そうしないと、実行時に例外NotImplementedErrorを生成します。
その前に抽象モデルから
先の連載で紹介したゲーム(バズルゲーム)に共通する特性を抽出して、新たに親クラスGameItemを規定します。
class GameItem(Shape):
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return repr(self)
def __repr__(self):
return "(%i,%i)"%(self.x, self.y)
def isExist(self, x, y):
return (self.x, self.y) == (x, y)
def width(self, g):
return g.clipBounds.width / self.dim()
def height(self, g):
return g.clipBounds.height / self.dim()
メソッドisExistは、指定した座標x/yに、実体(子孫クラスのインスタンス)が存在するかどうか判定した結果をリターン値とします。先の連載では、その名前をdetectとしましたが、関心の分離の趣旨に沿って、目的と手段の使い分けを利用者(プログラマー)に示唆するために、リファクタリングを実践しています。
メソッドwidth/heightは、パネルの大きさが変化しても、モノの大きさ(幅/高さ)を適切に保つために必要な情報を提供します。
GameItemの役割には、注意が必要です。子孫クラスに共通するフレームワークを提供しますが、具体的な特性は子孫に依存します。そのため、変数selfが参照する実体はどれも、子孫クラスのインスタンスであり、実際の処理は実行時に確定します。Javaにおけるabstract宣言されたクラスと違って「抽象クラスはインスタンスを生成できない」という制約こそありませんが、Smalltalkと同様に、抽象クラスのインスタンスを生成することに意味はありません。その判断は、コンパイラーではなく、プログラマーの英断に委ねられます。抽象クラスと、abstract宣言されたクラスとの違いについては、ブログ「ひよ子のきもち」で紹介しています。ようやく具象モデルに
class Stone(GameItem):
def __init__(self, x, y, state):
GameItem.__init__(self, x, y)
self.state = state
def __repr__(self):
if self.state == None:
s = self.state
else:
s = ("black", "white")[not self.state]
return "(%s,%s)"%(GameItem.__repr__(self), s)
def dim(self):
return OthelloPanel.dim
クラスStoneは、インターフェイスShapeに従って、メソッドを再定義します。メソッドdimは、マス目の数OthelloPanel.dimをリターン値とします。
dimは、インターフェイスShapeで抽象メソッドとして規定してあるので、これを再定義するのは、具象クラスStoneの責務です。利用者(プログラマー)は、これを積極的に活用することで、その実現方法に依存しない、柔軟性/拡張性に優れたコードを記述できます。しかし、この問題解決の手法には、工夫の余地があります。配列に値を保持させるのではなく、その対象を「オブジェクト」として実現します。表/裏の状態にある石は、単なる真偽値ではありません。自分がどの状態にあるべきかは、石自身で判断します。つまり、オブジェクト自身が「思考」するというわけです。

ゲームを開始するときに、左上に置かれた石は、x座標 3、y座標 3、白の面を上にしたインスタンスと見なせます。ここで、石の左上の隅を、その座標値とします。インスタンスに固有の情報を文字列として出力すると、次のようになります。
((3,3),True)
この結果は、メソッド__str__で規定した文字列表現から得られます。
class Stone(GameItem):
def paint(self, g):
self.paintBackground(g)
self.paintItem(g)
def paintBackground(self, g):
width = self.width(g)
height = self.height(g)
x = self.x * width
y = self.y * height
g.color = Color.green
g.fillRect(x, y, width, height)
g.color = Color.black
g.drawRect(x, y, width, height)
メソッドpaintは、インターフェイスShapeに従って、盤面を描画します。実際の処理は、補助関数paintBackground/paintItemに委ねます。
補助関数paintBackgroundは、盤面の背景(緑 Color.green)と罫線(黒 Color.black)を描きます。
