【実践UML】ロールプレイングゲームでクラス設計してみよう

design-class

クラス設計、むずかしいです。いろいろな人によって、いろいろな考え方によって、設計の仕方はまちまちです。

考え方を理解するには、身近なお題で設計してみると、よい練習になります。ロールプレイングゲームでクラス設計してみましょう。

目次

まずはユースケース

プログラム設計の最初に、ユースケースを整理することは多いです。プログラムに要求される仕様を整理します。

こんなユースケース記述で考える

まずは、シンプルなユースケースで考えましょう。

項目内容
ユースケース名バトル
アクタープレイヤー
事前条件
事後条件以下のいずれかとなる
・スライムのHPが0になる
・勇者のHPが0になる
メインフロー(1)プレイヤーが「たたかう」コマンドを入力する。
(2)勇者がスライムに攻撃。スライムのHPを、勇者の攻撃力の分減じる
(3)スライムのHPが0になったらバトル終了
(4)スライムが勇者に攻撃。勇者のHPを、スライムの攻撃力の分減じる
(5)勇者のHPが0になったらバトル終了
(6)(1)に戻る
備考今後は以下の拡張を予定
(ア)勇者が「にげる」コマンドを選択できるようにする。
(イ)一定確率で、スライムが先制攻撃してくる
(ウ)勇者の行動に「まほう」コマンドを追加し、魔法で攻撃できるようにする
ユースケース

よくあるバトルシーンですね。

簡単すぎる、と感じますか?ユースケース記述では、以下の2つが大事です。

  • 詳細に立ち入りすぎないこと
  • すべてを網羅しようと思わないこと

ロールプレイングゲームのような、自分がよく知っている仕様だと、いろいろなことが気になります。

  • 「HPの最大値はいくつ?」
  • 「やくそうで回復できるな」
  • 「地形によって攻撃力が変わったり」
  • 「攻撃をミスしたら…?」

思いつくままに、どんどん書き込み、何十ページにもわたる超大作になりがち。

ユースケースは抽象化が大事です。書き込みすぎてはいけません。1ページに収まらないユースケースなら、既にやりすぎです。

ユースケース図はあまり重要じゃない

ユースケースといえば、「ユースケース図」を思い浮かべがち。ですが実のところ、図を書いてもほとんど設計の役に立ちません

図は、ごまかしが効いてしまうのです。人マークと丸と矢印を配置すればそれっぽく見えますが、実は何も語っていなません。

「ユースケース記述」を書き、実際に言葉にして、仕様を考えましょう。

クラスを導出しよう

ユースケースから、クラスを導出しましょう。

勇者クラスとスライムクラス

勇者クラスとスライムクラスは、当然出てくるでしょう。ヒットポイントとこうげき力をプロパティに持たせます。

クラス図
クラス図

抽象化を急ぎすぎない

ここで先走って、こんなことを考えてしまいがち。

  • スライムはモンスターだから、モンスタークラスを作って継承して…
  • 勇者以外もいろいろ職業あるから、人間クラス作って…
  • 人間とモンスターの共通ベースにキャラクタークラス作って…

そして、壮大な継承ツリーを持つクラス図ができます。

やりすぎクラス図…
やりすぎクラス図

考えるのは楽しいんです。でも、今やることじゃない。

いきなり抽象化を考えない。まずは具体例で考える。

まずは、ユースケースを実現するための、必要最小限のクラス構成から考えましょう。

シーケンス図を書いてみる…

シーケンス図を書いてみましょう。深く考えずやると、こんな感じになりがち。

シーケンス図
シーケンス図1

プレイヤークラスが、何から何まですべてやっている状態。勇者クラスやスライムクラスは、単なるデータクラスになっています。

プレイヤークラスがなんでもやるのは、さすがによろしくない。バトルクラスを設けましょうか。

シーケンス図…
シーケンス図2

これで、バトルに関することはバトルクラスが受け持つようになりました。オーケーオーケー。

…さて、どうでしょうか?

クラスへの責任の分割を考える

上の例は、「トランザクションスクリプト」と呼ばれるアンチパターンです。バトルクラスがすべてをやりすぎ。

今後、どんな改修をするときもバトルクラスを直すことになります。そうするとバトルクラスがどんどん肥大化し、メンテナンス性が非常に悪くなります。

シーケンス図で、1つのクラスからやたら矢印が出ているときは要注意Tell, Don’t Ask に従って、他のクラスにも権限を委譲すべきです。

まず導出したクラスに責任を割り当て、クラスが妥当であるかを考えます。そして、

クラスの責任にかたよりがあるようなら、クラスへの責任の分割を考える。

ユースケースの役割をクラスに振り分ける

立ち戻って考えましょう。クラス分割において重要な考え方は、クラスに役割を振り分けること。

やり方として、クラス(オブジェクト)を、擬人化して考えると分かりやすいです。

クラス図
擬人化クラス図

責任は「なにを役割とする人か」、メッセージは「誰が誰に何を指示するか」です。

クラス設計での大事な考え方
  • クラス(オブジェクト)は、それが持つ責任で分割する。
  • クラス(オブジェクト)間のメッセージのやりとりを考える。

ユースケースの内容をクラスに振り分けましょう。

内容誰が誰に何を
「たたかう」コマンド入力battleバトル  playerプレイヤー コマンド入力を指示
勇者がスライムに攻撃battleバトル  hero勇者 攻撃を指示
スライムのHPを、勇者の攻撃力の分減じるhero勇者  slimeスライム ダメージ付与を指示
スライムのHPが0になったらバトル終了battleバトル  slimeスライム 生存確認を指示
スライムが勇者に攻撃battleバトル  slimeスライム 攻撃を指示
勇者のHPを、スライムの攻撃力の分減じるslimeスライム  hero勇者 ダメージ付与を指示
勇者のHPが0になったらバトル終了battleバトル  hero勇者 生存確認を指示

シーケンス図にしてみましょう。過度な矢印の集中がなくなっています。役割が分散されるようになりましたね。

シーケンス図
シーケンス図3

クラス(オブジェクト)を擬人化して、メッセージパッシングを考える。

コードを書く

実際にコーディングしてみましょう。Pythonでつくります。こんなかんじですね。

# 勇者
class Hero:
    # 初期化
    def __init__(self):
        self.hit_point = 20
        self.attack_power = 4
        self.is_alive = True

    # たたかう
    def attack(self, slime):
        slime.take_damage(self.attack_power)

    # ダメージ付与
    def take_damage(self, attack_power):
        self.hit_point -= attack_power
        self.is_alive = self.hit_point > 0


# スライム
class Slime:
    # 初期化
    def __init__(self):
        self.hit_point = 10
        self.attack_power = 2

    # たたかう
    def attack(self, hero):
        hero.take_damage(self.attack_power)

    # ダメージ付与
    def take_damage(self, attack_power):
        self.hit_point -= attack_power
        self.is_alive = self.hit_point > 0


# プレイヤー
class Player:
    # コマンド入力
    def input_command(self):
        print("1: たたかう")
        command_value = input()
        return command_value


# バトル
class Battle:
    # 初期化
    def __init__(self):
        self.player = Player()
        self.hero = Hero()
        self.slime = Slime()

    # バトル開始
    def start_battle(self):
        while True:
            print(f"ゆうしゃ HP:{self.hero.hit_point}")
            command_value = self.player.input_command()

            print("ゆうしゃのこうげき!")
            self.hero.attack(self.slime)
            if not self.slime.is_alive:
                print("スライムをたおした!")
                return

            print("スライムのこうげき!")
            self.slime.attack(self.hero)
            if not self.hero.is_alive:
                print("ゆうしゃはしんでしまった!")
                return


# メイン
def main():
    battle = Battle()

    battle.start_battle()


if __name__ == "__main__":
    main()

将来の変更に備える

次に、今後起こる変更も考えて、クラス設計が問題ないか確認しておきましょう。

ユースケースで定義した、今後の改修内容はこれですね。

今後の改修内容
  • 勇者が「にげる」コマンドを選択できるようにする。
  • 一定確率で、スライムが先制攻撃してくる
  • 勇者の行動に「まほう」コマンドを追加し、魔法で攻撃できるようにする

クラスにどのような変更が入るか、確認しておきましょう。

改修内容変更クラス変更内容
にげるコマンドplayerプレイヤー「にげる」コマンドを追加
battleバトルにげる処理を追加
先制攻撃battleバトル先制攻撃を追加
まほうコマンドplayerプレイヤー「まほう」コマンドを追加
hero勇者魔法攻撃を追加
slimeスライムまほうダメージ付与を追加

変更理由の責務が分かれているので、影響範囲が限定的になっていますね。

今後の拡張も想定したクラス設計としておく

クラス設計のときの大事な考え方

やりすぎないこと

設計のときに気にしておくことは、以下の3つ。

  • 具体的な例」で考える
  • 「やりすぎでないか」を常に気にする
  • 「今後直しやすいか」も確認する

そして、迷いが生じたときはの判断基準。

設計に迷ったら、よりシンプルなほうを取る

弱点がなくなったら完了、とすること

クラス設計は、きりがなくなります。どこで終わらせたらよいのでしょうか。

このように考えておくといいでしょう。

クラス設計は、正解を追い求めないこと。いま気づいている弱点がなくなったなら、完了とみなす

そして弱点とは、今後想定される拡張のやりやすさ、に関連します。

想定している拡張のこと「だけ」考えるのが重要です。なにを拡張するのかを、妄想で膨らませないこと。

「勇者のすべての生命力をかけた究極魔法…」なんて、いまの時点で設計に盛り込むことではないですね。

まとめ

クラス設計は、まったく無の状態から、ソフトウェアを生み出す過程です。当然ムズカシイです。

でも、やればやるほどクラス設計は上達します。いろいろなクラス設計を実践してみましょう。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次