クラス設計、むずかしいです。いろいろな人によって、いろいろな考え方によって、設計の仕方はまちまちです。
考え方を理解するには、身近なお題で設計してみると、よい練習になります。ロールプレイングゲームでクラス設計してみましょう。
まずはユースケース
プログラム設計の最初に、ユースケースを整理することは多いです。プログラムに要求される仕様を整理します。
こんなユースケース記述で考える
まずは、シンプルなユースケースで考えましょう。
項目 | 内容 |
---|---|
ユースケース名 | バトル |
アクター | プレイヤー |
事前条件 | - |
事後条件 | 以下のいずれかとなる ・スライムのHPが0になる ・勇者のHPが0になる |
メインフロー | (1)プレイヤーが「たたかう」コマンドを入力する。 (2)勇者がスライムに攻撃。スライムのHPを、勇者の攻撃力の分減じる (3)スライムのHPが0になったらバトル終了 (4)スライムが勇者に攻撃。勇者のHPを、スライムの攻撃力の分減じる (5)勇者のHPが0になったらバトル終了 (6)(1)に戻る |
備考 | 今後は以下の拡張を予定 (ア)勇者が「にげる」コマンドを選択できるようにする。 (イ)一定確率で、スライムが先制攻撃してくる (ウ)勇者の行動に「まほう」コマンドを追加し、魔法で攻撃できるようにする |
よくあるバトルシーンですね。
簡単すぎる、と感じますか?ユースケース記述では、以下の2つが大事です。
- 詳細に立ち入りすぎないこと
- すべてを網羅しようと思わないこと
ロールプレイングゲームのような、自分がよく知っている仕様だと、いろいろなことが気になります。
- 「HPの最大値はいくつ?」
- 「やくそうで回復できるな」
- 「地形によって攻撃力が変わったり」
- 「攻撃をミスしたら…?」
思いつくままに、どんどん書き込み、何十ページにもわたる超大作になりがち。
ユースケースは抽象化が大事です。書き込みすぎてはいけません。1ページに収まらないユースケースなら、既にやりすぎです。
ユースケース図はあまり重要じゃない
ユースケースといえば、「ユースケース図」を思い浮かべがち。ですが実のところ、図を書いてもほとんど設計の役に立ちません。
図は、ごまかしが効いてしまうのです。人マークと丸と矢印を配置すればそれっぽく見えますが、実は何も語っていなません。
「ユースケース記述」を書き、実際に言葉にして、仕様を考えましょう。
クラスを導出しよう
ユースケースから、クラスを導出しましょう。
勇者クラスとスライムクラス
勇者クラスとスライムクラスは、当然出てくるでしょう。ヒットポイントとこうげき力をプロパティに持たせます。
抽象化を急ぎすぎない
ここで先走って、こんなことを考えてしまいがち。
- スライムはモンスターだから、モンスタークラスを作って継承して…
- 勇者以外もいろいろ職業あるから、人間クラス作って…
- 人間とモンスターの共通ベースにキャラクタークラス作って…
そして、壮大な継承ツリーを持つクラス図ができます。
考えるのは楽しいんです。でも、今やることじゃない。
まずは、ユースケースを実現するための、必要最小限のクラス構成から考えましょう。
シーケンス図を書いてみる…
シーケンス図を書いてみましょう。深く考えずやると、こんな感じになりがち。
プレイヤークラスが、何から何まですべてやっている状態。勇者クラスやスライムクラスは、単なるデータクラスになっています。
プレイヤークラスがなんでもやるのは、さすがによろしくない。バトルクラスを設けましょうか。
これで、バトルに関することはバトルクラスが受け持つようになりました。オーケーオーケー。
…さて、どうでしょうか?
クラスへの責任の分割を考える
上の例は、「トランザクションスクリプト」と呼ばれるアンチパターンです。バトルクラスがすべてをやりすぎ。
今後、どんな改修をするときもバトルクラスを直すことになります。そうするとバトルクラスがどんどん肥大化し、メンテナンス性が非常に悪くなります。
シーケンス図で、1つのクラスからやたら矢印が出ているときは要注意。Tell, Don’t Ask に従って、他のクラスにも権限を委譲すべきです。
まず導出したクラスに責任を割り当て、クラスが妥当であるかを考えます。そして、
ユースケースの役割をクラスに振り分ける
立ち戻って考えましょう。クラス分割において重要な考え方は、クラスに役割を振り分けること。
やり方として、クラス(オブジェクト)を、擬人化して考えると分かりやすいです。
責任は「なにを役割とする人か」、メッセージは「誰が誰に何を指示するか」です。
- クラス(オブジェクト)は、それが持つ責任で分割する。
- クラス(オブジェクト)間のメッセージのやりとりを考える。
ユースケースの内容をクラスに振り分けましょう。
内容 | 誰が誰に何を |
---|---|
「たたかう」コマンド入力 | バトル | プレイヤー コマンド入力を指示
勇者がスライムに攻撃 | バトル | 勇者 攻撃を指示
スライムのHPを、勇者の攻撃力の分減じる | 勇者 | スライム ダメージ付与を指示
スライムのHPが0になったらバトル終了 | バトル | スライム 生存確認を指示
スライムが勇者に攻撃 | バトル | スライム 攻撃を指示
勇者のHPを、スライムの攻撃力の分減じる | スライム | 勇者 ダメージ付与を指示
勇者のHPが0になったらバトル終了 | バトル | 勇者 生存確認を指示
シーケンス図にしてみましょう。過度な矢印の集中がなくなっています。役割が分散されるようになりましたね。
コードを書く
実際にコーディングしてみましょう。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()
将来の変更に備える
次に、今後起こる変更も考えて、クラス設計が問題ないか確認しておきましょう。
ユースケースで定義した、今後の改修内容はこれですね。
- 勇者が「にげる」コマンドを選択できるようにする。
- 一定確率で、スライムが先制攻撃してくる
- 勇者の行動に「まほう」コマンドを追加し、魔法で攻撃できるようにする
クラスにどのような変更が入るか、確認しておきましょう。
改修内容 | 変更クラス | 変更内容 |
---|---|---|
にげるコマンド | プレイヤー | 「にげる」コマンドを追加 |
バトル | にげる処理を追加 | |
先制攻撃 | バトル | 先制攻撃を追加 |
まほうコマンド | プレイヤー | 「まほう」コマンドを追加 |
勇者 | 魔法攻撃を追加 | |
スライム | まほうダメージ付与を追加 |
変更理由の責務が分かれているので、影響範囲が限定的になっていますね。
クラス設計のときの大事な考え方
やりすぎないこと
設計のときに気にしておくことは、以下の3つ。
- 「具体的な例」で考える
- 「やりすぎでないか」を常に気にする
- 「今後直しやすいか」も確認する
そして、迷いが生じたときはの判断基準。
弱点がなくなったら完了、とすること
クラス設計は、きりがなくなります。どこで終わらせたらよいのでしょうか。
このように考えておくといいでしょう。
そして弱点とは、今後想定される拡張のやりやすさ、に関連します。
想定している拡張のこと「だけ」考えるのが重要です。なにを拡張するのかを、妄想で膨らませないこと。
「勇者のすべての生命力をかけた究極魔法…」なんて、いまの時点で設計に盛り込むことではないですね。
まとめ
クラス設計は、まったく無の状態から、ソフトウェアを生み出す過程です。当然ムズカシイです。
でも、やればやるほどクラス設計は上達します。いろいろなクラス設計を実践してみましょう。
コメント