はじめに
様々な言語で「デザインパターン」の本が世の中にありますが、筆者個人の経験では
いまいちピンとこない例
いまいちピンとこないコード
で説明されてることが多く、
結局これっていつ使うの?
という疑問に答えるには仕事仲間等との議論をしないと
辿り着けないことが多々ありました。
そこで特に「ゲーム開発ではどう使うか?」にフォーカスを当てて、実践的な例を交えて
デザインパターンの説明の需要があると思い記事を作りました。
デザインパターンを学ぶ理由
デザインパターンを学ぶ理由としては
- 車輪の再発明の防止
- 長文で読みにくいコード(可読性の低いコード)を減らす
- コードを疎結合にして変更に強くなる(変更時のコスト・変更箇所を減らす)
- モジュールとして使いまわせるように、コードの再利用性を高める
といった効果を期待できます。
対象読者
Unity 全くの初心者(インストールしただけで触ったことがないような方)はお断りです。
最低限以下のことは理解・経験を積んでおくことが必須になります。
- MonoBehaviour 継承クラスでコードを書いたことがある
- C# のピュアクラスを用いた自作クラスを作ったことがある
- クラスの継承という概念は知っている
そのため、脱・初心者
中級者へのステップアップ
として デザインパターンを学ぶ
のが良いと思います。
デザパタ記事リンク
生成系
構造系
様態・ふるまい系
- Chain of Responsibility パターン
- Command パターン
- Interpreter パターン
- Iterator パターン
- Mediator パターン
- Memento パターン
- Observer パターン
- State パターン
- Strategy パターン
- TemplateMethod パターン
- Visitor パターン
FactoryMethod パターンについて
FactoryMethodパターンについてWikipedia で調べると以下のように出てきます。
Factory Method パターンは、他のクラスのコンストラクタを
サブクラスで上書き可能な自分のメソッドに置き換えることで、
アプリケーションに特化したオブジェクトの生成をサブクラスに追い出し、
クラスの再利用性を高めることを目的とする。
(https://ja.wikipedia.org/wiki/Factory_Method_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3より)
これで理解できる方は以降の文章は読まなくて良いと思います(多分その方は抽象的にものを捉える天才だと思います)。
もう少し噛み砕いた言い方をすると以下のようになります。
Factory の抽象クラス or インターフェースを作って、生成メソッドの実装は派生先(インターフェース実装クラス)に任せてしまおうぜ。
身も蓋もない話をしてしまうと、 Factory をTemplateMethod パターンを使っていいかんじにしようぜ
ということです。
Abstract Factory と FactoryMethodの違い
抽象クラスを作る
という意味では AbstractFactory とFactoryMethod は 同じ
と認識して問題ありません。
ただし、両者の違いとしてはAbstractFactory は1種類以上のObject生成メソッド(API)を1つ以上持っている
と FactoryMethod はコンストラクタ相当のメソッド(API)しか持たない
というところでしょう。
少し具体的な例を考えてみましょう。
ここでは Builderパターン でも使った「スケルトンの敵」を用意することを考えてみましょう。
Abstract Factory とBuilder のおさらい
public abstract class BaseSkeletonEnemyFactory
{
public Enemy Create(){return new Skeleton();}
public abstract Weapon CreateWeapon();
public abstract Armor CreateArmor();
}
public class NormalSkeletonFactory : BaseSkeletonEnemyFactory
{
// 素のスケルトンは裸一貫
public override Weapon CreateWeapon(){return null;}
public override Armor CreateArmor(){return null;}
}
public class EliteWarriorSkeletonFactory : BaseSkeletonEnemyFactory
{
// Elite は銅の剣を持っている
public override Weapon CreateWeapon(){return new BronzeSword();}
// Elite は革防具を持つ
public override Armor CreateArmor(){return new LeatherArmor();}
}
public class SkeletonBuilder
{
BaseSkeletonEnemyFactory _factory = null;
public SkeletonBuilder(BaseSkeletonEnemyFactory myfactory)
{
_factory = myfactory;
}
public Enemy Build()
{
var e = _factory.Create();
var weapon = _factory.CreateWeapon();
if( weapon != null )
{
e.SetWeapon(weapon);
}
var armor = _factory.CreateArmor();
if( armor != null )
{
e.SetArmor(armor);
}
return e;
}
}
public static class GameLogic
{
public static void Main()
{
// ステージ1 用の設定
SkeletonBuilder[] builders = CreateLevel1EnemyBuilders();
foreach(var b in builders)
{
b.Build();
}
// ステージ2 用の設定
builders = CreateLevel2EnemyBuilders();
foreach(var b in builders)
{
b.Build();
}
}
public static SkeletonBuilder[] CreateLevel1EnemyBuilders()
{
// Level1 は雑魚スケルトン3体
return new SkeletonBuilder[]{
SkeletonBuilder( new NormalSkeletonFactory() );
SkeletonBuilder( new NormalSkeletonFactory() );
SkeletonBuilder( new NormalSkeletonFactory() );
}
}
public static SkeletonBuilder[] CreateLevel2EnemyBuilders()
{
// Level2 は雑魚スケルトン2体 + エリートスケルトン1体
return new SkeletonBuilder[]{
SkeletonBuilder( new NormalSkeletonFactory() );
SkeletonBuilder( new EliteWarriorSkeletonFactory() );
SkeletonBuilder( new NormalSkeletonFactory() );
}
}
}
上記では BaseSkeletonEnemyFactory
がAbstractFactory、SkeletonBuilderが Builderパターンで実装されていて、ゲームロジック側でそれぞれのLevelに合わせて敵の生成を行なっています。
BaseSkeletonEnemyFactory では スケルトン種族の敵
が必要な要素を全て提供する機能を有しています。
具体的には「素体のスケルトン」「装備させる武器」「装備させる防具」を提供するAPIがあります。
そしてBuilder はこれらのAPIを呼んで、必要に応じて装備させて最終的な敵スケルトンを作ってくれます。
Level1 では雑魚スケルトンが3体出てきて、Level2 では雑魚2体 + エリート1体という構成にして、実体の敵はBuilderを介して取得するようにしています。
敵をふやしたときの話
しかし、ゲームを作るとなると、もう少し敵を増やしたいところです。
ここではLevel2 をもう少し難しくするため「スライムの敵」を追加してみましょう。
public abstract class BaseSlimeEnemyFactory
{
public Enemy Create(){return new Slime();}
public abstract Color GetColor();
public abstract Vector3 GetSize();
}
public class GreenSlime : BaseSlimeEnemyFactory
{
public override Color GetColor(){return new Color32(0,255,0,255);}
public override Vector3 GetSize();{return Vector3.one;}
}
このような形で緑スライムを生成する工場を作ってみます。
ここでふとコードを見比べると、BaseSkeletonEnemyFactory もBaseSlimeEnemyFactory も
public Enemy Create()
というメソッドで素体を生成しています。
これを抽象化すると 「全ての敵は素体が必要で、それはCreate() で作る」
というふうに言い換える事ができます。
そこで以下のような1段階抽象的なEnemyFactoryを考えてみます。
public abstract class BaseEnemyFactory
{
public abstract Enemy Create();
}
このようにするとBaseSkeletonEnemyFactory とBaseSlimeEnemyFactory は以下のように書き換えられます。
public abstract class BaseSkeletonEnemyFactory : BaseEnemyFactory
{
public override Enemy Create(){return new Skeleton();}
public abstract Weapon CreateWeapon();
public abstract Armor CreateArmor();
}
public abstract class BaseSlimeEnemyFactory
{
public override Enemy Create(){return new Slime();}
public abstract Color GetColor();
public abstract Vector3 GetSize();
}
このようにすると、全ての敵はCreate() で作成可能
, だが どんな敵を生成するかは派生先に任せる
ことができます。これがFactory Method のやり方です。
FactoryMethod のメリット・デメリット
FactoryMethod はTemplateMethod パターンの利用例の一つです。
メリットとしては以下のことが挙げられます
- 抽象化してコードを書ける
-
- 実装先(具象クラス) への依存度を下げる
-
- コードを使う側が「どのクラスを使うべきか」という思考を減らす事ができる
- 実装漏れを防ぎやすい
抽象化による恩恵
ここは難易度がやや高めな話ではありますが、抽象化レベルをあげると「具象クラスへの依存度を減らす」ことと「コードを利用する側の負担を減らす」ことができます。
前者について、例えばGameLogic 部分で以下のように直接具象クラスを参照すると、以下のようなコードになります。
public static class GameLogic
{
public static void Main()
{
// ステージ1 用の設定
Enenmy[] enemyies = new Enenmy[]{
new NormalEnemySkeleton(),
new NormalEnemySkeleton(),
new NormalEnemySkeleton()
};
foreach(var e in enemyies)
{
e.SetUp();
}
// ステージ2 用の設定
enemyies = new Enenmy[]{
new NormalEnemySkeleton(),
new EliteEnemySkeleton(),
new NormalEnemySkeleton()
};
foreach(var e in enemyies)
{
e.SetUp();
}
}
}
このようにすると、Enemyの変更ごとにMain のロジックを修正しなくてはなりません。
そこで、ロジックとパラメータを分離する事で「既存ロジックに手を加えることなく、コードを修正する」ことができます。
より具体的に言えば Main()では今後、「敵の種族追加」や「登場的の変更」を行なってもコードを修正する必要がない
ということになります。
このようにすることで保守性を高めたり、ロジックのコードの可読性を上げることにつながります。
public static class GameLogic
{
public static void Main()
{
// ステージ1 用の設定
SkeletonBuilder[] builders = CreateLevel1EnemyBuilders();
// foreach内部ではBuilder しかみていないため、どんな敵が登場するのかなど考える必要がない
foreach(var b in builders)
{
b.Build();
}
// ステージ2 用の設定
builders = CreateLevel2EnemyBuilders();
foreach(var b in builders)
{
b.Build();
}
}
public static SkeletonBuilder[] CreateLevel1EnemyBuilders()
{
// Level1 は雑魚スケルトン3体
return new SkeletonBuilder[]{
SkeletonBuilder( new NormalSkeletonFactory() );
SkeletonBuilder( new NormalSkeletonFactory() );
SkeletonBuilder( new NormalSkeletonFactory() );
}
}
public static SkeletonBuilder[] CreateLevel2EnemyBuilders()
{
// Level2 は雑魚スケルトン2体 + エリートスケルトン1体
return new SkeletonBuilder[]{
SkeletonBuilder( new NormalSkeletonFactory() );
SkeletonBuilder( new EliteWarriorSkeletonFactory() );
SkeletonBuilder( new NormalSkeletonFactory() );
}
}
}
実装漏れの防止
これはTemplateMethod の特性というかC#の抽象クラスの特性にもなります。
抽象クラスでいえば abstract
キーワードで指定されたメソッドは派生先での実装をしないとコンパイルエラー
になります。
また、Interface も同様に、インターフェースのメソッドを定義しないとコンパイルエラー
になります。
このようにしておけば、派生先のクラスで「実装漏れによる不具合」を圧倒的に減らす事が可能です。
まとめ
FactoryMethod はTemplateMethod の一つです。
特にAbstract Factory をさらに抽象化するときに使ったり、抽象クラスではなく「インターフェースによるメソッド定義」も利用する事ができます。
種類が多くなりそうな機能の実装などに特に効果的で、処理をシンプルにすることに非常に役立つデザインパターンです。
参考
TechScore さん: https://www.techscore.com/tech/DesignPattern/FactoryMethod
Wikipedia: https://ja.wikipedia.org/wiki/Factory_Method_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3
実践Python3: https://www.oreilly.co.jp/books/9784873117393/