前回、前々回の実装を通して、将棋をするための基盤(Model)部分の設計についてそれなり考えられるようになってきました。前回前々回にやってたことはMVCにおけるModel部分ですが、今回は盤の部分や駒の操作(Moveみたいなこと)についてです。MVCのVも絡んだ、視点的にはちょびっとマクロな話題になります。思いついたことを書き殴っておくことにします。
まず、今回は簡易コードによってプロトタイプを書いています。成果物(ソース、実行結果)は後半の方に掲載していますので、ここに来られた方はまずそれを見てもらった方が私の考えが伝わりやすいかもしれません。まぁ、自分のグタグタした考えをつらつら並べるよりはコードを見てもらう方がわかりやすいだろうということですね。ちなみにこの取り組み自体はデザインパターンとか設計の勉強、という意味合いが結構強いです。使いどころとか、構成が適切かどうかという議論はさほど自信がないですが、とりあえず今回のコードではデザインパターンをいくつか使用しています。具体的には以下。
どの部分がそうなのか、とかいった話はコードと実行結果の後ろで述べます。とりあえず、以下しばらくは考察したこととかのメモに入ります。
Boardまわりのコードはほぼ(BoardをSubjectとした、Boardに対する)Observerが必須と考えます。少なくとも、局面が更新される類の操作が行われた際にはView、プレイヤー、棋譜ロガー、あとはタイマー、この辺りに変更を知らせる必要がある。これらの要素は局面の更新など、トリガーとなるイベントに連動する形で自身の処理を回すのが自然な発想かと思います。で、Boardの仕様変更(これは今後起こる可能性が大)の影響でまるごと実装やり直し、なんてのは勘弁してほしいので、少しでもBoardとその他の要素の間の結合を弱くしておく必要があるはずです。多分Observerが一番自然でしょう。
一度Boardを実装してみましたが、どうもピンと来ませんでした。"将棋ゲーム"どの情報をどのクラスで持つか、といった部分で話が難しくなってるのかな、と思います。
それともう1つ。確かOO原則のひとつに「1クラスには単一の責任のみ持たせよ」といった趣旨の教訓があったような気がします。Boardを実装してみてあまりよろしくないと感じたことの1つとして、考えなしに実装しているとBoardの責任が膨れ上がってしまうな、ということがあります。多分どんなプロジェクトでもそういう問題は発生するんでしょうけどね。リファクタリング本にもそういう注意喚起がありました。
で、考えた結果、Boardの責任は「駒の所在情報の保持」にほぼ専念させておくのが良いのではないか?getter/setterとか、isPiece(suji,dan)みたいな基本的なインタフェースは規定するものの、「駒を動かす」とか、(論理的な)操作についてはBoardの責任ではなく、Commandを使って責任を分離させてしまうのがいいんじゃないか?との考えに至りました。
Commandを使うことで、undo(≒待った)とかの実装もCommandの中で修正が効きます。
また、棋譜を採取する方法も割と考えやすくなります。LoggerをBoardをリッスンするObserverとして実装することはほぼ確定していますので、少なくとも棋譜関係のコードはBoardとは独立になる所がメリットです。実際ログ関係の部分はシステム基幹の実装とは無関係であるべきですし。Commandを使うことによって待ったのログも割とスマートに収集できると思います。コンピュータと対戦してるとつい待ったを使ってしまいたくなりますが、その辺のログもバッチリ残すよ、ってのは中々面白い機能だなと思います。
ここからは実際に書いてみたコードです。まず要件的なことを列挙しておきます。
クラス図とかはもし気が向けば後日掲載します。 キーとなるクラスは
の3つ。以下ソースです。
#!/usr/bin/env python # -*- coding: utf-8 -*- import string import re class Board: def __init__(self): self.observers = [] self.status = [] def registObserver(self, ob): if(not isinstance(ob, BoardObserver)): raise TypeError, "Board.registObserver(): arg is not BoardObserver" self.observers.insert(len(self.observers), ob) def notifyAll(self): for o in self.observers: o.update(self.status[-1]) def pushState(self, s): self.status.insert(len(self.status), s) def getTotalMove(self): return len(self.status) class BoardObserver: def update(self, stat): pass class Logger(BoardObserver): def __init__(self): self.kifu = [] def update(self, stat): self.kifu.insert(len(self.kifu), stat) def __iter__(self): for l in self.kifu: yield l def export(self): mblack = re.compile("^\+") mwhite = re.compile("^-") mresign= re.compile("^%RESIGN") s = "" for n,l in enumerate(self.kifu): if(n==0): s += "# 対局開始!\n" if(not mblack.match(l) is None): """ コメント行などもありうるので手数とファイル内の行数は本来一致しない。 が、その辺は簡易実装ということでスルー """ s += l+" #先手 "+str(n)+"手目\n" if(not mwhite.match(l) is None): s += l+" #後手 "+str(n)+"手目\n" if(not mresign.match(l) is None): s += "%Resign #投了\n" s += "# 対局終了" return s class View(BoardObserver): def update(self, stat): mb = re.compile("^\+") mw = re.compile("^-") mr = re.compile("^%RESIGN") if(not mb.match(stat) is None): print "(VIEW)Latest move BLACK: "+stat if(not mw.match(stat) is None): print "(VIEW)Latest move WHITE: "+stat if(not mr.match(stat) is None): print "(VIEW)RESIGN command accepted." class GameCommand: def __init__(self, board, cmdstring): self.board = board self.cmd = cmdstring def execute(self): print "Do something..." board.nofityAll() class CmdMove(GameCommand): def execute(self): print "[CmdMove] execute" self.board.pushState(self.cmd) self.board.notifyAll() class CmdResign(GameCommand): def execute(self): print "[CmdResign] execute" print "[CmdResign] TotalMove = "+str(self.board.getTotalMove()) self.board.pushState(self.cmd) self.board.notifyAll() def main(): bo = Board() logger = Logger() view = View() bo.registObserver(logger) bo.registObserver(view) print "[TEST]GAME START" print "=========================================" kflist = ["+7776FU","-3334FU", "+2625FU", "-8384FU", "%RESIGN"] expmove = re.compile("^[+-]") expspecial = re.compile("^%") for m in kflist: if(not expmove.match(m) is None): cmd = CmdMove(bo, m) if(not expspecial.match(m) is None): # 簡易コードなのでRESIGNのみを想定 cmd = CmdResign(bo, m) cmd.execute() print "===============GAME END================" print "\n*************************" print "[Logger] PRINT KIFU" for line in logger: print line print "\n*************************" print "[Logger.export()] PRINT KIFU EXPORTED FORM" print logger.export() if __name__=="__main__": main()
ViewやLoggerはBoardObserverを親に持っています。BoardObserverはJavaで言う所のinterfaceに相当します。もっと具体的に言うならAWTやSwingで登場するActionListenerとかです。Boardに登録されているリスナ(observer)が、ちゃんとObserverとしてのインタフェースを持ってることを保証してもらいます。また、GameCommandのサブクラスで着手(Move)や投了(Resign)の実装を行ってます。
これの実行結果は以下。
[TEST]GAME START ========================================= [CmdMove] execute (VIEW)Latest move BLACK: +7776FU [CmdMove] execute (VIEW)Latest move WHITE: -3334FU [CmdMove] execute (VIEW)Latest move BLACK: +2625FU [CmdMove] execute (VIEW)Latest move WHITE: -8384FU [CmdResign] execute [CmdResign] TotalMove = 4 (VIEW)RESIGN command accepted. ===============GAME END================ ************************* [Logger] PRINT KIFU +7776FU -3334FU +2625FU -8384FU %RESIGN ************************* [Logger.export()] PRINT KIFU EXPORTED FORM # 対局開始! +7776FU #先手 0手目 -3334FU #後手 1手目 +2625FU #先手 2手目 -8384FU #後手 3手目 %Resign #投了 # 対局終了
LoggerやViewは継承を使って拡張や変更を容易に行えることが分かっていただけるかなと思います。百聞は何とやらなので、以下のセクションでは実際にViewを拡張(変更)してみます。GUIは設計部分とは関係ないコードが増えるのでCUI上で変化をつけてみるに留めますが、GUIでも本質はそう変わらないはずですし拡張による影響がBoard等と無関係に行えることは理解いただけるはずです。実装する上でやることはViewを継承して独自のupdate()を実装するだけ。拡張ViewであるView2クラスは以下。
# 比較のため元のViewクラスも掲載 class View(BoardObserver): def update(self, stat): mb = re.compile("^\+") mw = re.compile("^-") mr = re.compile("^%RESIGN") if(not mb.match(stat) is None): print "(VIEW)Latest move BLACK: "+stat if(not mw.match(stat) is None): print "(VIEW)Latest move WHITE: "+stat if(not mr.match(stat) is None): print "(VIEW)RESIGN comand accepted." # 拡張のため新たに作成したView2クラス # ちょっとしたViewの変更などが発生してもこのようにサブクラス化で対応できる。 # Open-Closed の原則も満たしている。 class View2(View): def update(self, stat): mb = re.compile("^\+") mw = re.compile("^-") mr = re.compile("^%RESIGN") if(not mb.match(stat) is None): print "(VIEW)先手の着手: "+stat if(not mw.match(stat) is None): print "(VIEW)後手の着手: "+stat if(not mr.match(stat) is None): print "(VIEW)投了."
Viewを生成する側、すなわちmain側ではView()とView2()を切り替えるだけです。まぁこの辺もFactoryMethodを使うとかして抽象化した方がいい気はしますけど。main()のコードは元のやつからちょろっと改変してますが、それぞれの実行結果は以下。
===== Use View class ===== [CmdMove] execute (VIEW)Latest move BLACK: +7776FU [CmdMove] execute (VIEW)Latest move WHITE: -3334FU [CmdMove] execute (VIEW)Latest move BLACK: +2625FU [CmdMove] execute (VIEW)Latest move WHITE: -8384FU [CmdResign] execute [CmdResign] TotalMove = 4 (VIEW)RESIGN command accepted.
===== Use View2 class ===== [CmdMove] execute (VIEW)先手の着手: +7776FU [CmdMove] execute (VIEW)後手の着手: -3334FU [CmdMove] execute (VIEW)先手の着手: +2625FU [CmdMove] execute (VIEW)後手の着手: -8384FU [CmdResign] execute [CmdResign] TotalMove = 4 (VIEW)投了.
とまあこのようになります。将来の変更としてありうる感じなのは、ViewならUIの変更や追加、Loggerについてはサポートするフォーマットの追加や、同一フォーマットでエクスポート形式を変更・拡張(コンピュータの読み筋もコメントで含める形式にするとか)するなどが挙げられますね。Loggerのシナリオについてはレベルの違う拡張が含まれているので、棋譜まわりを真剣に実装するならもうちょい設計を考える余地があるでしょう。
また、今は棋譜に相当するリストを入力として読み上げるだけですが、この辺は工夫の余地大アリです。 入力文字列はだいたいCSAフォーマットに従うように記述してます。で、この構文をメインループ内で簡単に解析しているのですが、ViewとLoggerにもそのコードが存在しています。なんか同じ処理を繰り返している感じでセンスがないですね。本題とは違うので特に装飾とかは付けませんけど、この辺私が納得してないことはちょっと強調しておきたいです。このあたりの設計は多分Stateパターンあたりがマッチするんじゃないかと思いますが、まぁそれはいずれ。