あなたはこれから新しいシュミレーション RPG を開発することになりました.
シュミレーション RPG といえば, 多種多様なキャラクターを自軍に揃えたり, 各キャラクターを思い通りにカスタマイズできるその自由度の高さと戦略性が魅力ですよね!
あなたはそんなシュミレーション RPG でおなじみの「キャラクター」をモデル化してみることにしました.
よくあるアンチパターン
以下に挙げるのは 「よくありがちな, トラブルになりやすい例(アンチパターン)」 です.
よくあるクラス設計 (例)
まずはキャラクターをクラス化します.
各キャラクターは「戦士」「魔法使い」など何らかのジョブに就いているので, 各ジョブの共通部分を Character
という親クラスで共通化して, 具体的なジョブに関する情報を子クラスで表現することにしました.
「体力」・「腕力」・「魔力」などのおなじみのパラメータ一式については長くなるので割愛します.
コードは PHP です.
<?php
abstract class Character
{
/**
* このキャラクターの名前
* @var string
*/
private $name;
/**
* @param string $name このキャラクターの名前
*/
public function __construct($name)
{
$this->name = $name;
}
}
class Fighter extends Character
{
}
class Magician extends Character
{
}
抽象メソッドを利用して異なる処理を実装
サンプルコードは書きませんが, 各キャラクターが持つ「装備品」(Equipment
) についてもクラス化します.
ジョブごとに装備できるアイテムが異なるので, どのジョブが何を装備できるかを表す canEquip()
を実装しましょう.
<?php
abstract class Character
{
/**
* このキャラクターの名前
* @var string
*/
private $name;
/**
* @param string $name このキャラクターの名前
*/
public function __construct($name)
{
$this->name = $name;
}
/**
* このキャラクターが引数のアイテムを装備できるかどうかを判定します.
* @return bool
*/
abstract public function canEquip(Equipment $e);
}
class Fighter extends Character
{
public function canEquip(Equipment $e)
{
// 盾・鎧・兜などの重装系アイテムだったら true, それ以外は false
}
}
class Magician extends Character
{
public function canEquip(Equipment $e)
{
// 帽子やローブなどは true, それ以外は false
}
}
自然でいい感じですね! オブジェクト指向といえば継承. 継承ってすばらしい!
こんな感じでジョブクラスがどんどん追加され, また canEquip()
の他にもジョブ毎に異なる特技や耐性などといった情報が, 抽象メソッドとその継承という形で付与されていきました.
だんだん大変なことに……
無事にローンチしてから数ヶ月後の大型アップデートで, 各キャラクターに新たに「男性」「女性」という概念が追加されました.
さらに装備品の一種で「リボン」というアイテムが追加されました. これは女性であればジョブに関わらず誰でも装備可能という特性を持ちます.
諸々の条件を実装した結果, 当初のクラス設計はこんな感じに.
各ジョブの男女で共通な部分を元からあったクラスにまとめて, 各ジョブ (HogeHoge) の男女版として MaleHogeHoge と FemaleHogeHoge を新しく追加しました.1
<?php
abstract class Character
{
// 略
}
abstract class Fighter extends Character
{
public function canEquip(Equipment $e)
{
// 盾・鎧・兜などの重装系アイテムだったら true, それ以外は false
}
// その他, MaleFighter と FemaleFighter の共通部分の実装
}
abstract class Magician extends Character
{
public function canEquip(Equipment $e)
{
// 帽子やローブなどは true, それ以外は false
}
// その他, MaleMagician と FemaleMagician の共通部分の実装
}
class MaleFighter extends Fighter
{
public function canEquip(Equipment $e)
{
// 親クラスと同じ, 重装系アイテムのみ
return parent::canEquip($e);
}
}
class FemaleFighter extends Fighter
{
public function canEquip(Equipment $e)
{
// 重装系アイテムに加えてリボンも装備可能
return parent::canEquip($e) || $e->getType() === "ribbon";
}
}
class MaleMagician extends Character
{
public function canEquip(Equipment $e)
{
// 親クラスと同じく, 帽子やローブなど
return parent::canEquip($e);
}
}
class FemaleMagician extends Character
{
public function canEquip(Equipment $e)
{
// 帽子やローブなどに加えてリボンについては true, それ以外は false
}
}
このように, 元からあった Character のサブクラスがさらにその倍近く増えたことになります.
また, 新ジョブ「ダークナイト」追加!みたいなことになったら, DarkKnight
(男女の共通部分), MaleDarkKnight
, FemaleDarkKnight
をそれぞれ追加しないといけません.
なんとか乗り切った大型アップデートの 1 年後のテコ入れで, 「人種」という概念が追加されました!
例えば「戦士」ジョブの場合は「人間男性戦士」「人間女性戦士」「エルフ男性戦士」「エルフ女性戦士」「ドワーフ男性戦士」「ドワーフ女性戦士」から選べます……!
(血を吐いて倒れる)
さて, どうすればこの問題を解決できたでしょうか?
タイトルにもう答えが出てしまっていますが, この後追記する予定です. (2日には間に合わない予定)
-
今回の場合, Male* クラスで canEquip() をオーバーライドする必要はありません. クラス数の増大を強調するために敢えて入れました. ↩