はじめに
様々な言語で「デザインパターン」の本が世の中にありますが、筆者個人の経験では
いまいちピンとこない例
いまいちピンとこないコード
で説明されてることが多く、
結局これっていつ使うの?
という疑問に答えるには仕事仲間等との議論をしないと
辿り着けないことが多々ありました。
そこで特に「ゲーム開発ではどう使うか?」にフォーカスを当てて、実践的な例を交えて
デザインパターンの説明の需要があると思い記事を作りました。
デザインパターンを学ぶ理由
デザインパターンを学ぶ理由としては
- 車輪の再発明の防止
- 長文で読みにくいコード(可読性の低いコード)を減らす
- コードを疎結合にして変更に強くなる(変更時のコスト・変更箇所を減らす)
- モジュールとして使いまわせるように、コードの再利用性を高める
といった効果を期待できます。
対象読者
Unity 全くの初心者(インストールしただけで触ったことがないような方)はお断りです。
最低限以下のことは理解・経験を積んでおくことが必須になります。
- MonoBehaviour 継承クラスでコードを書いたことがある
- C# のピュアクラスを用いた自作クラスを作ったことがある
- クラスの継承という概念は知っている
そのため、脱・初心者
中級者へのステップアップ
として デザインパターンを学ぶ
のが良いと思います。
デザパタ記事リンク
生成系
構造系
様態・ふるまい系
- Chain of Responsibility パターン (本記事)
- Command パターン
- Interpreter パターン
- Iterator パターン
- Mediator パターン
- Memento パターン
- Observer パターン
- State パターン
- Strategy パターン
- TemplateMethod パターン
- Visitor パターン
ChainOfResponsibility パターンについて
ある処理をするにあたって、担当者の手にあまるような場合があります。
そのときに処理の一部を、より扱える担当者に任せる、そして次の担当者でも手に負えない場合は更に広範囲に扱える担当者に任せます。このように次々に処理を任せる場合、処理を任せると同時に、その処理を適切に行う 責任・責務
も一緒に移動します。
Chain of Responsibility = 責任の鎖、責任の連鎖 という命名の通り、権限がより広い対象に依頼をすることで処理を解決するようなふるまいのことを指し示します。
ポイントは、ただ処理を依頼するだけではなく、最初にAPIをコールされたものも、次の担当者も同一の 処理を終わらせる
という責任を連帯責任として負っているところです。
ゲームにおける ChainOfResponsibility
SingletonパターンやFacadeパターンで実装されるクラスは大抵は機能がたくさん詰め込まれる場合があります。
そのときに、一部処理をサブクラスに分離・委譲することで、コード量を減らして可読性・保守性を高めることが可能です。
このとき処理の流れとしては Facade
→ SubClass1
→ SubClass2
... と進み、返り値としては ...
→ SubClass2
→ SubClass1
→ Facade
→ 呼び出し元
といった順番で伝播します。
ただし、ゲーム文脈において、一般的に用いられる共通のインターフェースを用いたChainOfResponsibilityパターンでの実装はほとんど見受けられません。
しかし、ReactiveProgrammingではイベントの連鎖が発生し、ある種のChainOfResponsibilityのような動きで実装する場合があります。
UIイベント とChainOfResponsibility
Unityのボタンクラスではボタン入力時のイベントを IPointerClickHandler やUniRx のOnClickAsObservable で取得できます。
このイベント時の処理ですが、複雑な場合はUIを管理するクラスですべきではありません。そのため、親クラスなどにイベントを通知して処理を任せるように設計・実装することがあります。
具体的には以下のようなコードです
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 様々なUIRootのとりまとめクラス
/// </summary>
public class PageRoot
{
public void OnClickEvent(Button btn)
{
// Root側が色々重い処理を行う責務を追うべき
// ex)API通信などはRootが代理で行う
// ex)SaveData 更新等
}
}
/// <summary>
/// 様々なUIのとりまとめクラス
/// </summary>
public class UIRoot
{
private PageRoot m_parent = null;
public UIRoot(PageRoot parent)
{
m_parent = parent;
}
public void OnClickEvent(Button btn)
{
// Root側が色々重い処理を行う責務を追うべき
// ex)API通信などはRootが代理で行う
// ex)SaveData 更新等
// ここでも処理できない場合は更に上の階層で
m_parent?.OnClickEvent(btn);
}
}
/// <summary>
/// UIの管理クラス
/// </summary>
public class UI : MonoBehaviour
{
[SerializeField] private UIRoot m_uiRoot = null;
[SerializeField]
private Button m_btn = null;
void Start()
{
m_btn.onClick.AddListener(OnClick);
}
void OnClick()
{
m_uiRoot?.OnClickEvent(m_btn);
}
}
このように作っておく事で、UIはUIの制御に注力でき、UIRootではUI全体についての制御、そしてPageRootはUIRootでは取り扱えないような責任の大きい範囲のタスク(例えば他Singletonとのやりとり等)を行うというように、責任の大きさ単位で階層区切りを行うことが可能です。
まとめ
ChainOfResponsibilityは責任の連鎖が起きます。
がその責任は連鎖が続くほどにどんどん大きなものになります。(より上の権限を持った担当者に任せるため)
この クラスが負うべき責任
を考えるようになれることが、脱初心者で、中級者へのステップそのものになるので、あまり考えたことがない人は是非 責任
を考えることで設計をしてみましょう。