頻出する問題解決の設計パターンであるデザインパターンをいくつか説明します。
デザインパターンを知っておくことは、共通言語にもなり得ますし、設計の引き出しも増えるでしょう。
Strategyパターン
一連のアルゴリズムをカプセル化することで、それらを交換可能にするデザインパターンをStrategyパターンと言います。
アルゴリズムをカプセル化することで、クライアント側からアルゴリズム本体が独立するので、クライアント側を変更せずともアルゴリズムを変更できる利点があります。
実装例
振る舞いを定義したインタフェースを作成し、それを実装したクラスをフィールド変数として持つ(コンポジット)ことで振る舞いを「委譲」することができます。
例えば
- 打撃
- 斬撃
- 魔法
の3パターンの攻撃方法を動的に切り替えることができるプレイヤーを実装したいとします。
その場合クラス図は下記のようになります。
まずは、「攻撃する」という共通インタフェースを定義します。
namespace Strategy.Interface { public interface IAttack { void Attack(); } }
次に、IAttackインタフェースを実装した3パターンの攻撃を表すクラスをそれぞれ実装します。
using Strategy.Interface; using UnityEngine; namespace Strategy.Behaviour { public class Hit : IAttack { public void Attack() { Debug.Log("打撃攻撃!"); } } }
using Strategy.Interface; using UnityEngine; namespace Strategy.Behaviour { public class Slash : IAttack { public void Attack() { Debug.Log("斬撃攻撃!"); } } }
using Strategy.Interface; using UnityEngine; namespace Strategy.Behaviour { public class Magic : IAttack { public void Attack() { Debug.Log("魔法攻撃!"); } } }
そして、最後にIAttack型のフィールド変数を持つクラスを定義して、IAttack経由でそれぞれのクラスを利用します。
using Strategy.Interface; namespace Strategy.Client { public class PlayerAttack { private IAttack _attack; public PlayerAttack(IAttack attack) { _attack = attack; } public void Attack() { _attack.Attack(); } public void SetAttack(IAttack attack) { _attack = attack; } } }
このようにすることで、動的にプレイヤーの攻撃動作を切り替えることができます。 また、どうやって攻撃するかの詳細は各クラスに委譲しているので、PlayerAttack.csが詳細を知る必要がありません。
実際に下記のように使用してみます。
using Strategy.Behaviour; using Strategy.Client; using UnityEngine; namespace Strategy { public class PlayerAttackManager : MonoBehaviour { void Start() { var hit = new Hit(); var slash = new Slash(); var magic = new Magic(); // 初期状態は打撃攻撃に設定する var playerAttack = new PlayerAttack(hit); playerAttack.Attack(); // 斬撃攻撃に切り替える playerAttack.SetAttack(slash); playerAttack.Attack(); // 魔法攻撃に切り替える playerAttack.SetAttack(magic); playerAttack.Attack(); } } }
この出力は下記のようになると思います。
打撃攻撃! 斬撃攻撃! 魔法攻撃!
これで、「攻撃する」という処理をカプセル化することができ、それらを動的に交換可能にしました。
これがStrategyパターンです。
Decoratorパターン
オブジェクトに付加的な責務を動的に加える機能を実現するデザインパターンがDecoratorパターンです。
実装例
プレイヤーの攻撃に動的に属性を加えるという実装を例に解説します。
まずは「プレイヤーが攻撃する」という機能を定義したインタフェースとそれを実装した具象クラスを定義します。
namespace Decorator.Interface { public interface IPlayerAction { void Attack(); } }
using Decorator.Interface; using UnityEngine; namespace Decorator.Player { public class PlayerAction : IPlayerAction { public void Attack() { Debug.Log("攻撃します!"); } } }
次に、IPlayerActionを実装した抽象クラスを定義します。
これを継承することで様々な付加的責務を与えることができるサブクラスが作れるようになります。
using Decorator.Interface; namespace Decorator.Attribute { public abstract class PlayerActionAttr : IPlayerAction { protected IPlayerAction PlayerAction; public abstract void Attack(); } }
例えばこのPlayerActionAttrを継承した水属性の攻撃を付加するクラスは下記のようになります。
using Decorator.Interface; using UnityEngine; namespace Decorator.Attribute { public class Water : PlayerActionAttr { public Water(IPlayerAction playerAction) { base.PlayerAction = playerAction; } public override void Attack() { PlayerAction.Attack(); Debug.Log("水属性の攻撃!"); } } }
同じように毒属性の攻撃を付加するクラスも簡単に作れます。
using Decorator.Interface; using UnityEngine; namespace Decorator.Attribute { public class Poison : PlayerActionAttr { public Poison(IPlayerAction playerAction) { base.PlayerAction = playerAction; } public override void Attack() { PlayerAction.Attack(); Debug.Log("加えて毒属性のダメージ!"); } } }
では、これらをどのように使用すれば属性を動的に付加するコードを書けるのか。
using Decorator.Attribute; using Decorator.Interface; using Decorator.Player; using UnityEngine; namespace Decorator { public class PlayerActionManager : MonoBehaviour { private void Start() { IPlayerAction playerAction = new PlayerAction(); playerAction.Attack(); // PlayerActionをPoisonで装飾して毒属性を追加 playerAction = new Poison(playerAction); playerAction.Attack(); // さらに水属性を追加する playerAction = new Water(playerAction); playerAction.Attack(); } } }
このように、動的にオブジェクトをラップしていくことで付加的に責務を追加することができます。
このコードを実行してみると下記のように出力されます。
攻撃します! 攻撃します! 加えて毒属性のダメージ! 攻撃します! 加えて毒属性のダメージ! 水属性の攻撃!
属性攻撃が追加できていることがわかります。
このDecoratorパターンをクラス図にすると下記のようになります。
Commandパターン
リクエストをオブジェクトとしてカプセル化することで、一連のリクエストを一つのコマンドとして管理できたり、アンドゥ機能などの実装が容易になるデザインパターンがCommandパターンです。
実装例
Commandパターンはオブジェクトに対する処理のリクエストをコマンドとしてカプセル化するデザインパターンなので、まずは処理のリクエストをインタフェースとして定義します。
namespace Command.Interfaces { public interface ICommand { void Execute(); void Undo(); } }
次に、具象クラスを定義して、それに対する処理を定義しておきます。
using UnityEngine; namespace Command.Receiver { public class Light { public void On() { Debug.Log("Light On"); } public void Off() { Debug.Log("Light Off"); } } }
そして、コマンドインタフェースを実装した具象コマンドクラスを実現します。
using Command.Interfaces; using Command.Receiver; namespace Command { public class LightOnCommand : ICommand { private Light _light; public LightOnCommand(Light light) { _light = light; } public void Execute() { _light.On(); } public void Undo() { _light.Off(); } } }
このように、インタフェースを定義して、それを実装した具象クラスをCommandとして利用することで、マクロのように複数のコマンドをまとめて実行したり、アンドゥを実現したりと言うことが容易になります。
Facadeパターン
サブシステム(下位機能)を統合して、より使いやすい高水準インタフェースを提供するデザインパターンがFacadeパターンです。
実装例
まず、下記のようなサブシステムを考えます。
- 攻撃判定処理を行うPlayerAttack.cs
- エフェクト再生を行うAttackEffect.cs
- モーション再生を行うAttackMotion.cs
これらの機能を統合して「攻撃する」という処理を実現するPlayerAttackManager.csを作成する例を考えてみましょう。
まずは、サブシステム群を定義します。
using UnityEngine; namespace Facade.SubSystems { public class PlayerAttack { public void Attack() { Debug.Log("攻撃判定処理をします"); } } }
using UnityEngine; namespace Facade.SubSystems { public class AttackEffect { public void ShowAttackEffect() { Debug.Log("攻撃エフェクトを表示します"); } } }
using UnityEngine; namespace Facade.SubSystems { public class AttackMotion { public void PlayAttackMotion() { Debug.Log("攻撃モーションを再生します"); } } }
そしてこれらのサブシステムをコンポーズして構成される高水準インタフェースのPlayerAttackManager.csを定義します。
using Facade.SubSystems; namespace Facade { public class PlayerAttackManager { private PlayerAttack _playerAttack; private AttackEffect _attackEffect; private AttackMotion _attackMotion; public PlayerAttackManager(PlayerAttack playerAttack, AttackEffect attackEffect, AttackMotion attackMotion) { _playerAttack = playerAttack; _attackEffect = attackEffect; _attackMotion = attackMotion; } /// <summary> /// 攻撃判定・エフェクト再生・モーション再生 /// をカプセル化した「攻撃する」という処理を行うメソッド /// </summary> public void Attack() { _playerAttack.Attack(); _attackEffect.ShowAttackEffect(); _attackMotion.PlayAttackMotion(); } } }
このように定義することで、多数のサブシステムから構成される「攻撃する」という機能に対する簡潔なインタフェースが作成されました。
「攻撃する」という処理を実現したい場合は下記のように使用することができます。
using Facade.SubSystems; using UnityEngine; namespace Facade { public class PlayerAttackSimulator : MonoBehaviour { void Start() { var playerAttack = new PlayerAttack(); var attackEffect = new AttackEffect(); var attackMotion = new AttackMotion(); var playerAttackManager = new PlayerAttackManager(playerAttack, attackEffect, attackMotion); // サブシステムの機能をカプセル化した「攻撃する」という一連の処理を行うメソッド playerAttackManager.Attack(); // サブシステムの個々のメソッドもアクセスはできる playerAttack.Attack(); attackEffect.ShowAttackEffect(); attackMotion.PlayAttackMotion(); } } }
PlayerAttackManager.csのインスタンスメソッドを経由してサブシステム群から構成されている「攻撃する」という処理を簡単に実行することができています。
またFacadeパターンでは高水準インタフェースを提供するだけで、サブシステムを隠蔽するわけではありません。
よって、サブシステムのインスタンスメソッドも変わらず呼び出せるようになっています。
また、PlayerAttackManager.csのインスタンス化の際に渡す各サブシステムを差し替えることで内部動作を動的に変更することも可能です。
これは、Strategyパターンとも言えるでしょう。
最後にFacadeパターンの実装例をクラス図で表すと下記のようになります。
Stateパターン
オブジェクトの内部状態が変化した際の振る舞いを表すクラスを保持しておき、内部状態が変化するとその振る舞いが変更されるようにするデザインパターンがStateパターンです。
実装例
「プレイヤーの攻撃が、状態に応じて変化する」という実装例を考えてみましょう。
プレイヤーには下記の状態があるとします。
- 通常状態
- エンハンス状態(攻撃二倍)
- 麻痺状態(攻撃できない)
通常状態の攻撃後、一定確率でエンハンス状態に遷移します。
また、エンハンス状態での攻撃後一定確率で、通常状態か麻痺状態のどちらかに遷移します。
まずは、状態を表す抽象クラスを定義します。
全ての状態を表す具象クラスはこの抽象クラスを継承することになります。
using UnityEngine; namespace State.Abstract { public class AbstractPlayerState { protected PlayerAttack PlayerAttack; public virtual void Attack() { Debug.Log("攻撃方法が定義されていません"); } } }
この抽象クラスで定義されている仮想メソッドAttackをサブクラスで状態に応じた振る舞いになるようにオーバーライドします。
using State.Abstract; using UnityEngine; namespace State { public class PlayerNormalState : AbstractPlayerState { public PlayerNormalState(PlayerAttack playerAttack) { base.PlayerAttack = playerAttack; } public override void Attack() { Debug.Log("通常攻撃!"); if (Random.Range(0, 5) >= 2) { base.PlayerAttack.SetState(base.PlayerAttack.EnhanceState); } } } }
using State.Abstract; using UnityEngine; namespace State { public class PlayerEnhanceState : AbstractPlayerState { public PlayerEnhanceState(PlayerAttack playerAttack) { base.PlayerAttack = playerAttack; } public override void Attack() { Debug.Log("二倍の攻撃!!"); base.PlayerAttack.SetState(Random.Range(0, 2) == 0 ? base.PlayerAttack.ParalysisState : base.PlayerAttack.NormalState); } } }
using State.Abstract; using UnityEngine; namespace State { public class PlayerParalysisState : AbstractPlayerState { public PlayerParalysisState(PlayerAttack playerAttack) { base.PlayerAttack = playerAttack; } public override void Attack() { Debug.Log("痺れて攻撃できない"); base.PlayerAttack.SetState(base.PlayerAttack.NormalState); } } }
状態に応じたクラスをそれぞれ定義することで、各状態に応じた処理をクラスに委譲することができます。
なので、実際にプレイヤーの攻撃を管理するクラスは下記のように実装できます。
using System.Collections; using System.Collections.Generic; using State.Abstract; using UnityEngine; namespace State { public class PlayerAttack { public AbstractPlayerState NormalState { get; private set; } public AbstractPlayerState EnhanceState { get; private set; } public AbstractPlayerState ParalysisState { get; private set; } private AbstractPlayerState _currentState; public PlayerAttack() { NormalState = new PlayerNormalState(this); EnhanceState = new PlayerEnhanceState(this); ParalysisState = new PlayerParalysisState(this); _currentState = NormalState; } public void Attack() { _currentState.Attack(); } public void SetState(AbstractPlayerState state) { _currentState = state; } } }
Attackメソッドがかなり簡潔に実装できていることがわかります。
各状態を表すクラスにAttackメソッドの詳細を委譲することで、PlayerAttackクラスの実装が見やすくなりました。
ただ、一方で各状態ごとにクラスが必要になるので、クラスの数は膨大になるでしょう。
それによる全体像の把握のしやすさの低下と設計はトレードオフになります。
最後に、実際にPlayerAttack.csを利用するコードが下記の通りになります。
using UnityEngine; namespace State { public class PlayerAttackManager : MonoBehaviour { private PlayerAttack _playerAttack = new PlayerAttack(); void Start() { for (var i = 0; i < 10; i++) { _playerAttack.Attack(); } } } }
確率依存なところがあるので必ずしも下記の通りにはなりませんが、出力結果がそれらしいものが出ると思います。
通常攻撃! 二倍の攻撃!! 痺れて攻撃できない ... 二倍の攻撃!! 通常攻撃!
このように、各状態に応じたクラスを作り、上位のクラスでは状態を保持することだけに責務を止め、各状態に応じた処理の責務は下位のクラスにカプセル化することで、多数ある状態での処理を簡潔に書けるようなデザインパターン、Stateパターンを解説しました。
この実装例をクラス図にしたものが下記になります。
さいごに
デザインパターンについて学ぶ際に、下記の本を参考にさせて頂きました。
様々なデザインパターンについて、実装例とコミカルなストーリー付きで解説してくれているので、ハマる人には楽しんで学べる本だと思います。
逆に、「参考書は常に必要なことだけ記載しておいて欲しい」という方には雰囲気が合わないかもしれません…
また、デザインパターンはあくまで手段です。
使わずとも綺麗な設計ができる場合もありますし、無理やり使うことでデメリットを被る場合もあります。
ご利用は計画的に🙇♂️