実践で学ぶオブジェクト指向① 神経衰弱ゲームを設計する



「理解していない」という事実を分かってなかった……


自分が今までオブジェクト指向設計を全く考慮せず、不安定なrubyの知識の上でMVCを理解しないままアプリケーションを積み上げていた、という衝撃の事実を先週知りました。

「コントローラーに処理を全部書いて、メソッドとして切り出せる部分は切り出してモデルに移す」ことでファットコントローラーを回避していると思いこんでいました……。
プログラミングを初めて8ヶ月くらい経ってます。独学は危ないね……

ということで、「オブジェクト指向設計実践ガイド」を読んだので実践してみたいと思います。




神経衰弱チュートリアルを元に

kiiino44.web.fc2.com
以前paizaで問題を解きながら、「神経衰弱ゲーム作ったらおもしろそう!」と閃いて見つけたページです。
これに習って作成したアプリケーションを元に、オブジェクト指向設計の実践に挑戦します。


このチュートリアルではSinsuiというクラスを1つ作り、そこにすべての処理を記述して神経衰弱ゲームを作成しています。

    #         カード  表 / 裏 : +(表)、-(裏)
    #         マーク  : h(ハート)、d(ダイア)、c(クラブ)、s(スペード)、
    #         数字    : 16 進数 ( 1 ~ d )    
    @cards = "-h1-h2-h3-h4-h5-h6-h7-h8-h9-ha-hb-hc-hd"
    @cards << "-d1-d2-d3-d4-d5-d6-d7-d8-d9-da-db-dc-dd"
    @cards << "-c1-c2-c3-c4-c5-c6-c7-c8-c9-ca-cb-cc-cd"
    @cards << "-s1-s2-s3-s4-s5-s6-s7-s8-s9-sa-sb-sc-sd"


ポイントになるのは、52枚のカードの裏表を含めて3文字で表現している部分です。
その3文字をランダムに羅列した文字列がカードの並びを表します。
(トランプをプログラムに落とし込むこの発想に感動しました……)
これはこのまま使います。





はじめてのシーケンス図

さて、まずはシーケンス図を作成するところから


最初に思いついたのはこんな感じ

f:id:naito-coding0322:20190126060006p:plain

openメッセージを送るPlayer, すべてのカードを表す:cardsとその中で表にしたカードである:open属性を持つBoard というドメインオブジェクトと、カードの判定を行う概念的な?Checkerクラスを想定しました。


何かがおかしい……

Checkerクラスが「既に引いたカードが2枚に達しているかの判定」「引いた2枚が一致しているかの判定」という2つの責任を負っています。
これをそれぞれCounterクラスとJudgeクラスに分けます。
そして、Boardオブジェクトが属性別に2つに別れてしまっています。
これはシーケンス図としておかしいので、1つにまとめます。
だいぶスッキリしました。


シンプルなシーケンス図

f:id:naito-coding0322:20190126060026p:plain


グッとシンプルになりましたが、どうもBoardが知識を持ちすぎている気がします。
「カードをオープンしてほしい」というメッセージを受けたBoardが、「既にオープンしたカードが2枚あるかどうかCounterに確認する必要がある」ということを知っているのは好ましくないです。



全知全能の神、Game Masterの誕生


f:id:naito-coding0322:20190127015204p:plain

openメッセージを受けた後、openedなカードが2枚あるかどうかCheckerに確認し、無ければ1枚をオープン。
再度Checkerに枚数を確認し、2枚になったらJudgeにメッセージを送って2枚の数字が一致しているかを判定してもらい、不一致であればopenedなカードを削除するよう指示を出す。



上記のような処理を行うGame_Masterクラスが爆誕しました。
神経衰弱を進行させるために何が必要かを知り尽くしています。
しかしどうやるかの部分についてはそれぞれのオブジェクトに任せています。

こんな全知全能の神をアプリケーションの世界に召喚して良いのかどうか、正直わかりません。
そしてこのタイミングで、opened属性をBoardからPlayerに移しました。
オープンにしたカードが一時的にプレイヤーの所持物(データ)になる、というような解釈です。
それ以前の構成だとBoardの責任が2つあるように感じてこのように変更しました。


こうなると、図のPlayerの位置が適切でないような気がしてきます。
一連の処理の開始は、プレイヤーがゲームマスターにopenメッセージを送るのが適切だと思います。

それを踏まえて最終的に次のようになりました。



最終的な設計



f:id:naito-coding0322:20190127021718p:plain

ちなみにシーケンス図だけをあげていますが、実際にはシーケンス図を書く度にそれに沿って実際のコードを変更しています。
試行錯誤の連続で、この最終的な設計図にたどり着くまで丸2日ほどかかりました。
設計力は実際に頭を使って考えてコードを書いてを繰り返して鍛えないと身につかないらしいので、初めはこんなもんかなとも思いますが……。

でも、ただ一度だけ動くプログラムを書くのではなく、オブジェクト指向設計に基づいて世界を作り上げ、そのなかでプログラムを組み立ててアプリケーションを動かすようなこの営みはかなり楽しいですね。「設計はアート」と書いてあったのも頷けます。


この後は最終的に行き着いたコードを査定して、依存関係や各クラスが持っている(持っているべきではない)知識について考えようと思います。



各クラスの責任



Player オープンになったカードの情報を保持し、showアクションからGame Masterにopenメッセージを送る

Game Master 神経衰弱ゲームの段階を判断(処理分岐)し、各クラスにメッセージを送る

Counter openedが2枚に達しているかを判定する

Judge 渡されたカード2枚が一致しているかを判定する

Board 52枚のカードの並びを裏表含めて表現する