はじめに
伝統的なスタイルを卒業して、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
)を描きます。