FactoryMethodパターンでボタン生成をしてみる@Unity

本記事は[Craft Egg Advent Calendar 2021](https://qiita.com/advent-calendar/2021/craftegg)の12/10分の記事です（投稿が遅れ申し訳ありません）。
12/9の記事は@aluminum1981さんの「[瓶の中に入っている液体の簡易表現を実装してみる@Unity](https://qiita.com/aluminum1981/private/968f523f3cbc13345f4c)」でした。

## FactoryMethodについて
「Virtual Constructor」とも呼ばれ、<b>コンストラクタの代わりになるメソッドを作成、利用するパターン</b>です。

Wikipediaでは、
> Factory Method パターンは、他のクラスのコンストラクタをサブクラスで上書き可能な自分のメソッドに置き換えることで、 アプリケーションに特化したオブジェクトの生成をサブクラスに追い出し、クラスの再利用性を高めることを目的とする。

とあります。

生成するオブジェクトごとにファクトリがあり、生成処理はサブクラスで行うため、生成処理を簡潔にすることができます。


### クラス図
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/8/8e/Factory_Method_UML_class_diagram.svg/1920px-Factory_Method_UML_class_diagram.svg.png" width="500px">
（[Wikipedia](https://ja.wikipedia.org/wiki/Factory_Method_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3)より）

FactoryMethodパターンを用いる上で、以下の4つの要素が登場します。

* 抽象クラス
    * <b>Creator</b><br>
      作成者（ファクトリ）のことで、Productをインスタンス化するためのインタフェースを定義する
    * <b>Product</b><br>
       ファクトリで生成したいオブジェクトのインタフェースを定義する
* 具象クラス
    * <b>ConcreteCreator</b><br>
      生成したいオブジェクトをインスタンス化する（生成するConcreateProductクラスの種類だけある）
    * <b>ConcreteProduct</b><br>
      ファクトリで生成したいオブジェクトの実装をしているクラス

## 実装してみる
ボタンを例に、シンプルなC#コードで実装してみます

### Product
製品の抽象クラスです。

``` cs
public abstract class ButtonBase
{
    public string Name { get; protected set; }
    public string Label { get; private set; }
    protected Action onClick;

    // ラベル設定
    public void SetLabel(string label)
    {
        Label = label;
    }

    // ボタン押下時コールバック設定
    public void RegisterOnClickCallback(Action onClick)
    {
        this.onClick = onClick;
    }

    // クリック処理
    public void Click()
    {
        onClick?.Invoke();
    }
}
```

### Creator
製品を生成する抽象クラスです。
具体的な内容はContreteCreatorが実装し、<b>実際に生成するConcreateCreatorの事は何も知りません</b>。

``` cs
public abstract class ButtonFactory
{
    public ButtonBase Create(string label, Action onClick)
    {
        ButtonBase button = createButton(label, onClick);
        return button;
    }

    protected abstract ButtonBase createButton(string label, Action onClick);
}
```

### ConcreateProduct
2種の製品を生成する比較のため、通常ボタンと特別ボタンを用意してみました。

```cs
public class NormalButton : ButtonBase
{
    public NormalButton()
    {
        Name = "通常ボタン";
    }
}

public class SpecialButton : ButtonBase
{
    public SpecialButton()
    {
        Name = "特別ボタン";
    }
}
```

### ConcreteCreator
実際の生成クラスです。
通常ボタンと特別ボタンの生成の処理に違いを持たせています。


``` cs
public class NormalButtonFactory : ButtonFactory
{
    protected override ButtonBase createButton(string label, Action onClick)
    {
        NormalButton button = new NormalButton();
        button.SetLabel(label);
        button.RegisterOnClickCallback(onClick);
        return button;
    }
}

public class SpecialButtonFactory : ButtonFactory
{
    protected override ButtonBase createButton(string label, Action onClick)
    {
        SpecialButton button = new SpecialButton();

        // 特別なボタンはラベルに☆が付きます
        button.SetLabel($"☆{label}☆");

        // 特別なボタンはクリック時音が鳴ります
        onClick += playSpecialSound;
        button.RegisterOnClickCallback(onClick);

        return button;
    }

    private void playSpecialSound()
    {
        Debug.Log("キラリと音が鳴った！");
    }
}
```

### 呼び出し側
```cs
private ButtonBase normalButton;
private ButtonBase specialButton;

public void Start()
{
    NormalButtonFactory normalButtonFactory = new NormalButtonFactory();
    normalButton = normalButtonFactory.Create("押してね", onClickNormalButton);
    Debug.Log($"{normalButton.Name}をラベル「{normalButton.Label}」と表示させ作成しました。");

    SpecialButtonFactory specialButtonFactory = new SpecialButtonFactory();
    specialButton = specialButtonFactory.Create("押してね", onClickSeecialButton);
    Debug.Log($"{specialButton.Name}をラベル「{specialButton.Label}」と表示させ作成しました");
}
}

public void ClickNormalButton()
{
    normalButton.Click();
}

public void ClickSpecialButton()
{
    specialButton.Click();
}

private void onClickNormalButton()
{
    Debug.Log("NormalButtonがクリックされました");
}

private void onClickSeecialButton()
{
    Debug.Log("SpecialButtonがクリックされました");
}
```

### 実行結果
```text:Start()
通常ボタンをラベル「押してね」と表示させ作成しました。
特別ボタンをラベル「☆押してね☆」と表示させ作成しました
```

```text:ClickNormalButton()
NormalButtonがクリックされました
```

```text:ClickSpecialButton()
SpecialButtonがクリックされました
キラリと音が鳴った！
```

生成するにあたっての<b>固有の処理をサブクラスであるConcreateCreatorに持たせ、呼び出し側やCreatorはその内容を知らずに済む</b>実装ができました。

## Unity上でボタンとして実装する
先述したコードをベースに、Unityに実際のボタンとして生成させてみます。

コードはConcreateCreatorと呼び出し側のみ紹介し、他は省略しますが、以下のような変更を行いました。
 
* Product
    * SerializeFieldでuGUIのButton, Textをアタッチ
    * Unityの方法に従ってラベルやOnClickを登録（AddListenerなど）
* Creator
    * 生成時の位置を指定できるように、Createの引数にTransformを追加
* ConcreateProduct
    * コンストラクタの処理をUnityのAwake()に置き換え
    * SpecialButtonはエフェクトを出すメソッドを作成（詳細は後述）



```cs:ConcreateCreator
public class NormalButtonFactory : ButtonFactory
{
    private const string PrefabPath = "NormalButton";

    protected override ButtonBase createButton(Transform root, string label, Action onClick)
    {
        GameObject prefab = (GameObject)Resources.Load(PrefabPath);
        NormalButton button = GameObject.Instantiate(prefab, new Vector3(0, 0, 0), Quaternion.identity).GetComponent<NormalButton>();
        button.transform.SetParent(root, false);

        button.SetLabel(label);
        button.RegisterOnClickCallback(onClick);
        return button;
    }
}

public class SpecialButtonFactory : ButtonFactory
{
    private const string PrefabPath = "SpecialButton";

    protected override ButtonBase createButton(Transform root, string label, Action onClick)
    {
        GameObject prefab = (GameObject)Resources.Load(PrefabPath);
        SpecialButton button = GameObject.Instantiate(prefab, new Vector3(0, 0, 0), Quaternion.identity).GetComponent<SpecialButton>();
        button.transform.SetParent(root, false);

        // 特別なボタンはラベルに☆が付きます
        button.SetLabel($"☆{label}☆");

        // 特別なボタンはクリック時エフェクトが出ます
        onClick += button.PlayEffect;
        button.RegisterOnClickCallback(onClick);

        return button;
    }
}

```

```cs:呼び出し側
[SerializeField]
private Transform normalButtonRoot;

[SerializeField]
private Transform specialButtonRoot;

public void Start()
{
    NormalButtonFactory normalButtonFactory = new NormalButtonFactory();
    ButtonBase normalButton = normalButtonFactory.Create(normalButtonRoot, "押してね", onClickNormalButton);
    Debug.Log($"{normalButton.Name}を作成しました。");

    SpecialButtonFactory specialButtonFactory = new SpecialButtonFactory();
    ButtonBase specialButton = specialButtonFactory.Create(specialButtonRoot, "押してね", onClickSeecialButton);
    Debug.Log($"{specialButton.Name}を作成しました");
}

private void onClickNormalButton()
{
    Debug.Log("NormalButtonがクリックされました");
}

private void onClickSeecialButton()
{
    Debug.Log("SpecialButtonがクリックされました");
}

```

![GIF 2021-12-19 20-05-42.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146248/b72cae12-adc6-833a-ae88-7b8600e568d9.gif)

音を鳴らすのはGIFでは伝わらないため、特別ボタンを押すとキラキラ画像が出るように仕様変更しました。


※画像素材は以下のサイト様よりお借りしました。
>七三ゆきのアトリエ　https://nanamiyuki.com/11631

## 終わりに
デザインパターンの一つであるFactoryMethodを実装してみました。
生成に関する処理をクラスごとに記述し、利用側はそれを意識したり書き換える必要がないのが便利です。

今回はボタンを紹介しましたが、アウトゲームにおけるUIパーツ（ダイアログ、サムネイル、アイテムのアイコン etc...）など、色々な種類があって頻繁に生成されるものに対して有効なパターンの一つだと思いました。

## 参考
https://ja.wikipedia.org/wiki/Factory_Method_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3

https://qiita.com/shoheiyokoyama/items/d752834a6a2e208b90ca

https://qiita.com/i-tanaka730/items/1f521866c7d699ddd093