社内勉強会で使った資料です
Agenda
- コードにおけるプロトタイプ
- データモデリングにおけるプロトタイプ
1. コードにおけるプロトタイプ
- デザインパターンのプロトタイプ
- あらかじめ用意しておいた「原型」からインスタンスを生成するようにするためのパターン
- オブジェクトの生成者をサブクラスにすることを回避する
-
new
で新しいオブジェクトを作ることによるコストが高すぎる時にそれを回避する
プロトタイプのない世界 / ある世界
ない世界
// RPGゲームの怪物クラスの基底クラス
class Monster
{
// 中身・・・
};
// 怪物クラス達
class Ghost : public Monster {};
class Demon : public Monster {};
class Sorcerer : public Monster {};
// 怪物を生成するスポナークラスの基底クラス
class Spawner
{
virtual ~Spawner() {}
virtual Monster* spawnMonster() = 0;
}
// 各怪物のスポナークラス達
class GhostSpawner : public Spawner
{
public:
virtual Monster* spawnMonster();
{
return new Ghost();
}
}
class DemonSpawner : public Spawner
{
public:
virtual Monster* spawnMonster();
{
return new Demon();
}
}
class SorcererSpawner : public Spawner
{
public:
virtual Monster* spawnMonster();
{
return new Sorcerer();
}
}
問題点
- たくさんのクラスがあり、決まり文句の処理があり、同じ処理があり、重複があり、何度も同じことの繰り返しでイカしてない
-> これをプロトタイプパターンが解決する
- 鍵となる着想は、オブジェクトは自分自身に似たオブジェクトを生成できるということ
- どの怪物も、自分自身と同じ型を持つ怪物を生成するためのプロトタイプとして扱える
ある世界へと変革
- プロトタイプの仕組みを実装するために、基底クラスに抽象メソッド
clone()
を作成
class Monster
{
virtual ~Monster() {}
virtual clone() = 0;
// その他のコード・・・
};
// 継承側
class Ghost : public Monster
{
public:
Ghost(int health, int speed)
: health_(health),
speed_(speed)
{}
virtual Monster* clone()
{
return new Ghost(health_, speed_);
}
private:
int health_;
int speed_;
};
class Spawner
{
public:
Spawner(Monster* prototype)
: prototype_(prototype)
{}
Monster* spawnMonster()
{
return prototype_->clone();
}
private:
Monster prototype_;
};
サンプルコードを見てわかる良いところ
- 各怪物用のスポナークラスを用意する必要がなくなった
- クラスの複製を作成するだけでなく、状態も複製できる
- 素早い動きの幽霊、弱い幽霊、遅い幽霊など、幽霊のプロトタイプを適切に作成すれば様々なスポナーを作成できる
ただし...
- 各々の怪物クラスに
clone()
を実装する必要がある- 実際あまりコード量が変わっていない...
- きちんと
clone()
を実装しようとした場合、「深い」複製にするか「浅い」複製にするか考える必要がある - 今日ではほとんどのゲームエンジンがこんな方法をとっていない!!
その他の解決方法
1. 生成関数
- 各怪物用に生成するための関数を作成
Monster* spawnGhost()
{
return new Ghost():
}
// スポナークラス
typedef Monster* (*spawnCallback)();
class Spawner()
{
public:
Spawner(SpawnCallback spawn)
: spawn_(spawn)
{}
Monster* spawnMonster() { return spawn_(); }
private:
SpawnCallback spawn_;
};
// 生成関数の生成
Spawner* ghostSpawner = new Spawner(spawnGhost);
- 先程のスポナークラススはインスタンス丸ごと持たせていたが、今回は関数へのポインタ1つ持つだけでよくなった
2. テンプレート
- スポナークラスはある型のインスタンスを生成したいが、特定の怪物クラスをコード内で明示したくない
-> この問題の解決方法として、型パラメータを使う方法があり、テンプレートを使えばそれが可能
class Spawner
{
public:
virtual ~Spawner() {}
virtual Monster* spawnMonster():
};
template <class T>
class SpawnerFor : public Spawner
{
public:
virtual Monster* spawnMonster() { return new T(); }
};
// 使い方
Spawner* ghostSpawner = new SpawnFor<Ghost>();
結論
- C++では型は第1級オブジェクトでないので先程のような細工が必要になるが、jsやRubyと言った動的型付けされる言語ではクラスも通常のオブジェクトであるため、簡単に解決できる
- このような選択肢を考え合わせた上での結論としては、プロトタイプデザインパターンが最良の解決方法であるとは思えない(筆者談)
2. データモデリングにおけるプロトタイプ
- コードにおけるプロトタイプはあまり良い点を見出せなかったが、データモデリングにおいては役に立つことがある
2. データモデリングにおけるプロトタイプ
- もし、ゲームファイル中の全てのバイトを数えてコードとデータに分けて比較できたとしたら、初期に比べると現在はコードはゲームを動かすエンジンに過ぎず、内容は全てデータにあるというゲームが多くなっている
- それは素晴らしいことだが、大規模プロジェクトをどう管理するかという問題が解決するわけではなく、むしろ問題は難しくなる
-> その問題の解決方法の1つがプロトタイプ
プロトタイプない世界/ある世界
ない世界
# 戦士ゴブリンのデータ
{
"name": "goblin grunt",
"minHealth": 20,
"maxHealth": 30,
"resists": ["cold", "poison"],
"waknesses": ["fire", "light"],
}
# 魔法使いゴブリンのデータ
{
"name": "goblin wizard",
"minHealth": 20,
"maxHealth": 30,
"resists": ["cold", "poison"],
"waknesses": ["fire", "light"],
"spells": ["fire ball", "lightning bolt"]
}
# その他ゴブリンのデータ・・・
問題点
- このデータ管理方法だと、無駄に場所を使うし作成するのに余計な時間がかかってしまう
- ゴブリン全部強くするとなった場合に全て修正する必要が出てしまう
-> 共通化しよう
-> でもコードなら継承でどうにかなるけど、JSONにはそんな芸当無理
-> ここでプロトタイプの考え方!
考え方
- オブジェクトに
prototype
というフィールドを追加し、このオブジェクトが委任する別のオブジェクトを保持していることを宣言する - このオブジェクト内に存在しない属性のアクセスがあった場合は定義したプロトタイプの中を探す仕組みにする
# 戦士ゴブリンのデータ
{
"name": "goblin grunt",
"minHealth": 20,
"maxHealth": 30,
"resists": ["cold", "poison"],
"waknesses": ["fire", "light"],
}
# 魔法使いゴブリンのデータ
{
"name": "goblin wizard",
"prototype": "goblin grunt",
"spells": ["fire ball", "lightning bold"]
}
# その他ゴブリンのデータ・・・
出てくる疑問
- ここで基底となるゴブリンデータを用意していないのは、プロトタイプベースのシステムでは、どのオブジェクトからでも複製で新たなオブジェクトを作れるのが当然であるため
- ゲームで出てくるアイテムやモンスターは基本、普通の要素を特殊化したものであることが多いので、このような定義をするときはプロトタイプと委任が合っている