デザインパターンの勉強を始めたんだけど、ファクトリーパターンって何ができるの??
ファクトリーパターンは何かできるわけではないよ
移植性・拡張性を高めるための手法なんだ!
そうなんだ!
よくわからないね!!
よし、今日はファクトリーパターンの解説をしよう!
プログラミングを学んでいると目にすることになるデザインパターン、本やGoogleを見ても出てる例のイメージわかないし理解が難しいですよね。
本記事ではUnityでのプログラミングに焦点を当てて初心者コードからFactoryパターン適用までの道筋を解説します。
初心者でも分かるよう手順を追って解説してはいますが、デザインパターンは若干アドバンスな面がありますので本記事の対象者は下記を想定します。
では解説をはじめましょう。まずは結論を簡単にご紹介します。
インスタンス生成ロジックを管理しやすくするパターン
- オブジェクト生成のロジックまわりをスッキリ整理するための知恵
- 生成ロジックをFactoryパターンへ引き渡すことができる
- 生成するモノの種類が増えても過去のソースを変更しなくていいのが利点
冒頭でも触れましたが、SingletonパターンやObject Poolパターンなどとは違って直接的な効果が見えづらいですね。大丈夫です、順を追って説明します!
Factoryパターンはどんなとき使うのか、メリットは?
Factoryパターンを使うメリットは3つあります。
- 生成ロジックをまとめることができる
- 生成するオブジェクトの種類が増えても対応しやすい(拡張性)
- パターンの構造を転用しやすい(移植性)
なので、
- チーム開発である
- 個人ではあるが開発規模が大きくなる予定だ
という前提のもと、「けっこう種類が増えていきそうなオブジェクト」に対して適用するのが良いです。
理解して導入するのが少し大変なので、個人の小規模な開発では無理に使わなくてもよいと思います。
拡張性が低いとはどういうことか
Factoryパターンはインスタンス生成まわりの拡張性を高める手法です。
まずは拡張性が低い状態のイメージをみてみましょう。2Dアクションを作っていて、敵A、敵B、宝箱、ハテナブロックからコインが出るとしましょう。
宝箱からコインが出る部分をコードで書くとこんな感じでしょう。インスペクタから非アクティブなコインを設定しておいて、Instantiateしたあとにアクティブに設定しています。
Chest .cs
using UnityEngine;
using UnityEngine.EventSystems;
public class Chest : MonoBehaviour
{
[SerializeField] GameObject Coin;
public void OpenChest()
{
// コインを出す
GameObject go = Instantiate(Coin, transform.position, Quaternion.identity);
go.SetActive(true);
// 宝箱を消す
Destroy(gameObject);
}
}
同様に、敵A、敵B、ハテナブロックも作っていきます。中身は全部同じです。
EnemyA.cs
public class EnemyA : MonoBehaviour
{
[SerializeField] GameObject Coin;
public void DropCoin()
{
// コインを出す
GameObject go = Instantiate(Coin, transform.position, Quaternion.identity);
go.SetActive(true);
// 敵を消す
Destroy(gameObject);
}
}
EnemyB.cs
public class EnemyB : MonoBehaviour
{
[SerializeField] GameObject Coin;
public void DropCoin()
{
// コインを出す
GameObject go = Instantiate(Coin, transform.position, Quaternion.identity);
go.SetActive(true);
// 敵を消す
Destroy(gameObject);
}
}
ItemBlock.cs
public class ItemBlock : MonoBehaviour
{
[SerializeField] GameObject Coin;
public void DropCoin()
{
// コインを出す
GameObject go = Instantiate(Coin, transform.position, Quaternion.identity);
go.SetActive(true);
// ブロックを消す
Destroy(gameObject);
}
}
ではコインが出現したときにコインが回転するエフェクトを出したくなったとしましょう。どこを変更すればいいでしょうか? そう、敵A、 敵B、 宝箱、はてなブロックにあるInstantiate周辺を全部ですね。これがもし敵の種類が30だとしたら….考えただけでも恐ろしい物量です。
このように、生成ロジック(Instantiate周辺)を生成したい側(敵、宝箱)に実装すると仕様を変えたときの修正箇所が膨大になりとても変更に弱くなります。これではうかうかと敵や宝箱の種類を増やせませんね。これが拡張性の低い状態のイメージです。
Factory Patternの道のり
さて本題に入りましょう。拡張性が低いさきほどの例にFactory Patternに向けて改造していきます。
ここからFactoryパターン適用までの道のりを解説しますので少し冗長かもしれません。
Factoryパターンとは何かを先にみたい方は「Factory パターンの概説」の章をご覧ください。
オブジェクト生成専用クラスを作る
さきほどの例ではコインを欲しい側でそれぞれInstantiate していることが理由で変更に弱く拡張性が低い状態になっていました。
ではどうすればよいかというと、シンプルに生成する専用のクラスを作って一括管理します。生成したい側に直接Instantiateを書いているとロジックが散らばってしまって問題でしたので、1ヶ所にまとめて管理しようということです。
例として先程の続きで、敵や宝箱から指示を受けてコインを出すスポナーを作ってみましょう。
CoinSpawner.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CoinSpawner : MonoBehaviour
{
[SerializeField] GameObject Coin;
public void SpawnCoin(Vector2 position)
{
// コインを出す
GameObject go = Instantiate(Coin, position, Quaternion.identity);
go.SetActive(true);
// ★ここにコインを回転する処理をいれることができます★
}
}
使う側はこんな感じです。
Chest.cs (宝箱)
using UnityEngine;
using UnityEngine.EventSystems;
public class Chest : MonoBehaviour
{
[SerializeField] CoinSpawner coinSpawner; // インスペクタから設定
public void OpenChest()
{
// コインを出す
coinSpawner.SpawnCoin(transform.position);
// 宝箱を消す
Destroy(gameObject);
}
}
EnemyA.cs
using UnityEngine;
using UnityEngine.EventSystems;
public class EnemyA : MonoBehaviour
{
[SerializeField] CoinSpawner coinSpawner; // インスペクタから設定
public void OpenChest()
{
// コインを出す
coinSpawner.SpawnCoin(transform.position);
// 敵を消す
Destroy(gameObject);
}
}
EnemyB、ItemBlockも同様です。
コインを出したい側からコインスポナーに処理を引き渡すことで、もしコインを回転したくなっても修正箇所はスポナー1つですむようになりました。
なぜなら、敵A、B、宝箱、ハテナブロックに散在していたInstantiateがCoinSpawnerのSpawnCoinの1か所にまとまったからです。
Factory共通の処理を作れるように継承を活用する
これでコインの生成ロジックが一括管理できるようになりました。次のステップはFactory側を、拡張しやすいよう整理していく流れを見てみましょう。
たとえば、コインの種類を増やして大きいコインや赤いコインを出したくなったとしましょう。小さいコインは回転少し、大きいコインはたくさん回転、赤コインはパーティクルも出したいです。
コインスポナーのC#スクリプトをコピーして作れそうですね。
CoinSpawnerS.cs (小さいコイン)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CoinSpawnerS : MonoBehaviour
{
[SerializeField] GameObject Coin; //小さいコイン
public void SpawnCoin(Vector2 position)
{
// コインを出す
GameObject go = Instantiate(Coin, position, Quaternion.identity);
go.SetActive(true);
// ★ここにコインを少し回転★
}
}
CoinSpawnerL.cs (大きいコイン)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CoinSpawnerL : MonoBehaviour
{
[SerializeField] GameObject Coin; //大きいコイン
public void SpawnCoin(Vector2 position)
{
// コインを出す
GameObject go = Instantiate(Coin, position, Quaternion.identity);
go.SetActive(true);
// ★ここにコインをたくさん回転★
}
}
CoinSpawnerRed.cs (赤コイン)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CoinSpawnerRed : MonoBehaviour
{
[SerializeField] GameObject Coin; //赤色のコイン
public void SpawnCoin(Vector2 position)
{
// コインを出す
GameObject go = Instantiate(Coin, position, Quaternion.identity);
go.SetActive(true);
// ★ここにパーティクルを生成★
}
}
これでコインの種類に応じたスポナーを増やすことができました。
しかしまだ拡張性は低い状態です。
例として変更を加えてみましょう。Instantiateしたあとに必ずSetActive している部分に注目してみます。これはヒエラルキーにある非アクティブ状態のコインをもとにInstantiate して使っているからで、Instantiate 後にアクティブにセットしています。
使わないオブジェクトがシーン上にいるのもよくないので、シーンのオブジェクトを使わずPrefabを使うように変更してみましょう。その場合、SetActiveは不要になりますね。変更する箇所は、コイン小、コイン大、赤コイン・・・おっとまた拡張性が低いですね。
これを共通化してまとめる方法があります、ずばり継承です。
同じ処理をしている部分をまとめて基底クラスをつくり、もともとあったクラスはサブクラスとして実装します。具象クラスから継承元の基底クラスを作るので汎化ですね。
基底クラスには共通の処理を定義しておきます。
- Instantiateする部分
- Prefabを格納するGameObject
また、Prefabを使うことにしたのでSetActiveは消しておきます。
そうすると、基底クラスCoinSpawnerはこんな感じになります(さっきのCoinSpawner.csを基底クラスにします)。
CoinSpawner.cs
using UnityEngine;
public abstract class CoinSpawner : MonoBehaviour
{
/// <summary>
/// コインのPrefab
/// </summary>
[SerializeField] protected GameObject Coin; // コインのPrefab
/// <summary>
/// コインを取得します
/// </summary>
public void SpawnCoin(Vector2 position)
{
// コインを出す
GameObject go = InstantiateCoin(Coin, position);
// 出した後の処理(エフェクトなど)
AfterSpawn(go);
}
protected abstract void AfterSpawn();
/// <summary>
/// コインをPrefabからInstantiateします
/// </summary>
protected GameObject InstantiateCoin(GameObject prefab, Vector2 position)
{
GameObject go = Instantiate(prefab, position, Quaternion.identity);
return go;
}
}
ポイントは外部公開しているSpawnCoin()内でInstantiateCoin()してからAfterSpawn()を呼ぶという手順を定義している部分です。
InstantiateCoin()はデフォルトの動作を書いてますがprotectedにしてあるのでサブクラスで独自に定義することができます。特定のコインだけ生成処理が違ったりしてもサブクラスで実装できます。
AfterSpawnメソッドは、コインを出した後のエフェクトをサブクラスで実装できるようにする仕掛けです。この仕掛けもまたデザインパターンでTemplate Methodパターンと呼ばれます。
AfterSpawnのほかにスポナーとして共通の処理があるならば基底クラスに書いておきます。
たとえばSingleton化やObject Pool利用などの応用がありますね。
さて、基底クラスCoinSpawnerを継承してコイン小スポナー、コイン大スポナー、コイン赤スポナーを作っていきましょう。
CoinSpawnerS.cs(出現時にちょっと回転)
using DG.Tweening;
using UnityEngine;
public class CoinSpawnerS : CoinSpawner
{
const float rotate_duration = 0.8f;
const float rotate_angle_aroundY = 540f;
protected override void AfterSpawn(GameObject go)
{
go.transform.DOLocalRotate(Vector2.right * rotate_angle_aroundY, rotate_duration);
}
}
CoinSpawnerL.cs(出現時にたくさん回転)
using DG.Tweening;
using UnityEngine;
public class CoinSpawnerL : CoinSpawner
{
const float rotate_duration = 1.2f;
const float rotate_angle_aroundY = 720f;
protected override void AfterSpawn(GameObject go)
{
go.transform.DOLocalRotate(Vector2.right * rotate_angle_aroundY, rotate_duration, RotateMode.LocalAxisAdd);
}
}
CoinSpawnerRed.cs(出現時にパーティクルを出す)
using UnityEngine;
public abstract class CoinSpawnerRed : CoinSpawner
{
[SerializeField] ParticleSystem particle;
protected override void AfterSpawn()
{
Instantiate(particle, transform.position, Quaternion.identity);
}
}
少し手間ですがこれでコイン出現の共通処理を基底クラスで表現し、各コイン(大小赤)固有の処理はサブクラスで実装することができました。
これによりInstantiate周りの変更はスポナー1か所で済み、コインの種類が増えたとしても具象スポナーをコピーして好きな回転やパーティクルを書くだけで対応することができるようになりました。
すなわち、拡張性が高まったのです。おさらいすると、ここでやったことは2つです。
・基底クラスのスポナーに生成ロジックをまとめた
・コイン種類ごとのスポナーを作った
そして、次のステップでコインにコイン出現の固有処理を渡すことでFactory側はなにも処理本体を実装せずにとてもシンプルなかたちになります。
コインも基底クラスを作る
ここまでくればあと一歩です。コインが複数種類あるので先ほどCoinSpawner基底クラスを作ったように、コインにも基底クラスを作っておきます。
同一カテゴリで複数種類ある場合は基底クラスを作って共通の変数やメソッドを定義しておくのがセオリーです。たとえば、コインを拾ったときのエフェクトやもらえる金額はどの種類のコインであっても必要ですから基底クラスに定義しておきます。
using DG.Tweening;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Coin : MonoBehaviour, ICoin
{
/// <summary>
/// コインを拾ったときに貰える金額
/// </summary>
public int MoneyAmount = 100;
/// <summary>
/// コインを拾ったときにプレイヤから呼ばれる<br></br>
/// </summary>
/// <param name="pos"></param>
public void OnTaken(Vector2 pos)
{
transform.DOScale(0, 1f); // 徐々に消える
// ここで音を鳴らしたりパーティクルを出したりする
Invoke("Disappear", 1f); // 1秒後にDestory
}
/// <summary>
/// コインを消滅させる<br></br>
/// </summary>
private void Disappear()
{
Destroy(gameObject);
}
/// <summary>
/// コインが生成された直後に実行する初期化処理
/// </summary>
/// <exception cref="System.NotImplementedException"></exception>
public virtual void Initialize()
{
// 継承先で実装する
}
}
クラス宣言のところにあるインターフェイスICoinは「InitializeメソッドがCoinには絶対必要である」と決めています。
ICoin.cs
public interface ICoin
{
/// <summary>
/// コイン生成したときに何かする<br></br>
/// たとえばエフェクトを出すなど
/// </summary>
public abstract void Initialize();
}
これによりCoin派生クラスは必ずInitializeが存在すると担保できますので、安心してスポナー側からCoin.Initialize()を使えます。Initializeにはスポナー側に仮でおいていたCoin固有の出現エフェクトを実装します。スポナー側ではCoinのInitialize を呼び出すだけですむようになります。
基底クラスCoinができましたので、継承してコイン大、コイン小、赤コインを作ります。
これらのサブクラスでは各コイン特有の部分であるInitialize()を書いていきます。
CoinS.cs (コイン小、出現時に少し回転)
using UnityEngine;
using DG.Tweening;
public class CoinS : Coin
{
const float rotate_duration = 0.8f;
const float rotate_angle_aroundY = 540f;
public override void Initialize()
{
transform.DOLocalRotate(Vector2.right *540, 0.8f);
}
}
CoinL.cs (コイン大、出現時に大きく回転)
using DG.Tweening;
using UnityEngine;
public class CoinL : Coin
{
const float rotate_duration = 1.2f;
const float rotate_angle_aroundY = 720f;
public override void Initialize()
{
transform.DOLocalRotate(Vector2.right * rotate_angle_aroundY, rotate_duration, RotateMode.LocalAxisAdd);
}
}
CoinRed.cs (コイン赤、出現時にパーティクル)
using DG.Tweening;
using UnityEngine;
public class CoinRed : Coin
{
[SerializeField] ParticleSystem particle;
public override void Initialize()
{
Instantiate(particle, transform.position, Quaternion.identity);
}
}
作ったスクリプトはコインそれぞれのPrefabにアタッチします。
そして各スポナーのCoinプレハブにインスペクタから設定しておきます。
コイン側にInitialize()を設けたので、スポナー側はこうなります。
CoinSpawnerS.cs
using UnityEngine;
public class CoinSpawnerS : CoinSpawner
{
protected override void AfterSpawn(GameObject go)
{
// スポナー側のコイン出現エフェクトは削除
//go.transform.DOLocalRotate(Ve....
// 代わりにコインそのものにエフェクトを出してもらう
go.GetComponent<Coin>().Initialize();
}
}
CoinSpawnerL.cs
using UnityEngine;
public class CoinSpawnerL : CoinSpawner
{
protected override void AfterSpawn(GameObject go)
{
// スポナー側のコイン出現エフェクトは削除
//go.transform.DOLocalRotate(Ve......
// 代わりにコインそのものにエフェクトを出してもらう
go.GetComponent<Coin>().Initialize();
}
}
CoinSpawnerRed.cs
using UnityEngine;
public class CoinSpawnerRed : CoinSpawner
{
protected override void AfterSpawn(GameObject go)
{
// スポナー側のコイン出現エフェクトは削除
//Instantiate(partic....
// 代わりにコインそのものにエフェクトを出してもらう
go.GetComponent<Coin>().Initialize();
}
}
それぞれ見比べてみると、なんと生成したCoinサブクラスのInitializeを呼ぶ処理だけになり、どのスポナーも全く同じ内容になりました。
ということは、コインの種類が増えてもスポナーだけはコピペで対応できるということです。これが何を意味するかというと、「Instantiateを一手に引き受けることができる構造」が、「コピペだけで量産できる」ようなフレームワークになっているのです。これがファクトリーパターンのメリットの1つです。
お疲れ様です、これでFactory パターンを適用できました。最初のコードから考えて変更や拡張が容易になっているのがわかるかと思います。
ここまでのまとめ
ここまで、初心者コードを例に各所に散在していた生成処理をまとめ拡張しやすい構成に整理する流れを説明しました。整理した手順をまとめておきます。
- コイン出現の部分をCoinSpawner1か所にまとめた
- CoinSpawnerを汎化しコイン種類ごとにサブクラスを作った
- コインも基底クラスを作りサブクラスで出現エフェクトを実装
- CoinSpawnerでコイン側の出現エフェクトを呼び出し
慣れてくれば手順を追う必要はないので、最初からFactory パターンで設計するほうがよいと思います。
今回のコインスポナーの構成をクラス図に表すとこうなります。
Factory パターンの概説
前章ではコインを出現させる例で初心者コードからFactory Patternの適用までの流れを解説しました。
本章ではより一般的な概念としてFactory パターンの解説をします。
Factory パターンとは
Factory パターンはインスタンス生成のフレームワークの1つです。インスタンス生成は複雑な手順を踏むことが多くあり、欲しい側で都度そのロジックを実装していては変更と拡張に非常に弱くなってしまいます。そこで、インスタンス生成ロジックを手順(フレームワーク)と具体的な実装にを分けて管理できるようにした設計がこのFactory パターンです。
Factoryパターンには4つの構成要素があります。
- Factory
- ConcreteFactory
- Product
- ConcreteProduct
1つずつ見ていきましょう!
Factoryクラス
Productを生成する抽象クラスです。これを直接使うことはなく、各ConcreteProduct用に派生させて使います。
Factoryクラスの役割は3つあります。
- Factoryを使う側のためにGetメソッドを公開
クラス図ではGetProduct()がその役割 - Getで生成の手順を定義(実際のロジックをどの順で実行するか)
- サブクラスが実装すべき実際の生成ロジックを規定
クラス図ではfactoryMethodがその役割
コインスポナーの例でいうとCoinSpawner基底クラスがこれにあたり、その中のSpawnCoinがGetProductに相当します。
ConcreteFactoryクラス
各ConcreteProduct専用に派生させるFactoryのサブクラスです。
ConcreteFactoryクラスには次の役割があります。
- 各ConcreteProductに合わせた実際の生成ロジックを実装
クラス図でいうとfactoryMethodを実装したサブクラスということですね。
ConcreteProductの種類が1つ増えるごとに、対応したConcreteFactoryも1つ増やします。
なぜなら、Productの生成ロジックが種類ごとに違うという前提のパターンだからです。たまたま内容同じならコピペでクラス名だけ変えてOKです。
コインスポナーの例でいうとCoinSpawnerS 、CoinSpawnerL、CoinSpawnerRedです。
Productクラス
生成される側クラスの基底です。
Productクラスには次の役割があります。
- 全Productに共通する変数やメソッドを定義
クラス図ではmethod1, method2がProductの種類にかかわらず共通で存在すると規定しています。
コインスポナーの例でいうとCoin基底クラスにあたります。
プレイヤがコインを取得したときのエフェクトOnTakenがmethod1ですね。
また、Factoryが呼び出すコイン初期化メソッドをインターフェイスで定義してあります。
ConcreteProductクラス
Product派生クラスです。これをスマートに生成したいがためのデザインパターンです。
コインスポナーの例でいうとCoinS、CoinL、CoinRedクラスです。
ちなみに、Concreteというのは基底クラスが手順を定めているだけに対して、実際の使うクラスというような意味合いです。
Unity用にパターンをカスタマイズした部分
本記事で解説したコインの例は、一般的に語られるFactoryパターンとは違う部分があります。
一般的なProductは抽象クラスであるのに対し、例で出したコインクラスはICoinインターフェイスを実装した具象クラスです。
UnityではGameObjectインスタンス生成で直接newすることができません。GameObjectのPrefabが必要だからです。これによりCoin基底クラスを純粋なInterfaceにできません。そこでICoinインターフェイスでmethod1()であるInitialize()を定義しておき、そのICoinとMonoBehaviorを2つ継承した具象クラスCoinをProductの代替としています。こうすることでCoinSpawner側にGameObject(prefab)への参照をもちInstantiateすることができるようになります。
まとめ
本記事ではまず初心者コードからFactoryパターンまでの道筋を説明し、次に一般的なFactoryパターンの解説をしました。
使えるシーンが限られているテクニックではありますが、誰かのコードを読むときに出てきたりするので必要なときにまた読み返してください。
なおFactoryパターンはUnity公式の無料PDFでも紹介されています(英語)
https://blog.unity.com/games/level-up-your-code-with-game-programming-patterns
特にSOLIDの原則は読んでみるとデザインパターンへの理解が深まると思いますので読んでみるのもよいかと思います。
かわいい我が子にオリジナルアプリを!
コメント
とてもわかりやすかったため、参考になりました。他の記事も見てみたいと思います。
お褒めいただきありがとうございます!とても励みになります!