HeliScriptで疑似interfaceを作る

# 概要
オブジェクト指向プログラミングにかかせない**インターフェイスをクラスで代用**するコード例です。
実装の強制ができなかったりと限界があるので、大人しくHeliScriptがインターフェイスに対応するのを待った方いいかもしれません。

:::note info
**メリット**
- 使用クラスで実装クラス詳細を知らなくてよくなります
- 新しく実装クラスを追加しても、実装側に変更を加えなくてもよくなります
:::

# NPCダイアログのコード例
## 概観
DialogueService: このクラスを、ユースケースを実現する適当なクラス(ComponentClassなど)で使う想定

Diaogue: 選択肢なしのダイアログ

ChoiceDialogue: 選択肢付きのダイアログ

IDialogue: 上記ダイアログのインターフェイス(もどき)

DialogueData: セリフの生データをここに記述する

![image-20241118-013820.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14345/2f292d74-85ec-4fbc-b60f-b078d70d093a.png)

## ①インターフェイスと実装クラス（IDialogue/Dialogue/ChoiceDialogue）
### IDialogue
- 通常のinterfaceに記述するのはID()とLine()の部分になります
- コンストラクターでDialogueかChoiceDialogueのどちらか一方のみ注入できるようにしています
- AsDialogue()など、ダウンキャスト用のメソッドを用意してください

```c#
/// <summary>
/// ダイアログの疑似インターフェイス
/// </summary>
class IDialogue
{
    Dialogue _Dialogue;
    ChoiceDialogue _ChoiceDialogue;

    public void Construct(Dialogue dialogue, ChoiceDialogue choiceDialogue)
    {
        if (_Dialogue !== null && _ChoiceDialogue !== null)
        {
            hsSystemWriteLine("エラー: 依存性注入できるインスタンスは一つだけです");
            return;
        }
        _Dialogue = dialogue;
        _ChoiceDialogue = choiceDialogue;
    }

    // ---以下、インターフェイスの中身---

    public int ID()
    {
        int id = -1;
        if (_Dialogue !== null)
        {
            id = _Dialogue.ID();
        }
        else
        {
            id = _ChoiceDialogue.ID();
        }
        return id;
    }

    public string Line()
    {
        string line = "";
        if (_Dialogue !== null)
        {
            line = _Dialogue.Line();
        }
        else
        {
            line = _ChoiceDialogue.Line();
        }
        return line;
    }

    //---以下、ダウンキャスト用---

    public bool IsChoiceDialogue()
    {
        return _ChoiceDialogue !== null;
    }

    public Dialogue AsDialogue()
    {
        return _Dialogue;
    }

    public ChoiceDialogue AsChoiceDialogue()
    {
        return _ChoiceDialogue;
    }
}
```

### Dialogue/ChoiceDialogue
interfaceに定義したID()とLine()を実装してください。

:::note warn
**限界**
実装の強制ができないため、注意してください
:::

```c#
class Dialogue //:IDialogue
{
    int _ID;
    public int ID() { return _ID; }

    string _Line;
    public string Line() { return _Line; }

    int _NextDialogueId;

    public void Construct(int id, int nextDialogueId, string line)
    {
        _ID = id;
        _NextDialogueId = nextDialogueId;
        _Line = line;
    }

    public int GetNextDialogueId()
    {
        return _NextDialogueId;
    }
}

class ChoiceDialogue //:IDialogue
{
    int _ID;
    public int ID() { return _ID; }

    string _Line;
    public string Line() { return _Line; }

    list<int> _NextDialogueIds;

    public void Construct(int id, list<int> nextDialogueIds, string line)
    {
        _ID = id;
        _NextDialogueIds = nextDialogueIds;
        _Line = line;
    }

    public int GetNextDialogueIdByChoice(int choiceOrder)
    {
        return _NextDialogueIds[choiceOrder];
    }
}
```
## ①インターフェイスと実装クラス（IDialogue/Dialogue/ChoiceDialogue）
### DialogueData
:::note info
**メリット**
実装クラスでなく、IDialogueのリストを保持している点
:::
```c#
/// <summary>
/// NPC**１人分**のダイアログデータ
/// 設定がしやすいようにJsValで設定できるようにしている
/// </summary>
class DialogueData
{
    list<IDialogue> _IDialogues;
    public list<IDialogue> Value() { return _IDialogues; }

    public DialogueData()
    {
        _IDialogues = new list<IDialogue>(); 

        // keyがダイアログIDのJsVal
        JsVal val = new JsVal()
        {
            "0": {
                "type": "statement",
                "line": "こんにちは",
                "nextId": 1
            },
            "1": {
                "type": "choice",
                "line": "どうしますか？",
                "nextIds": [2, 1]
            },
            "2": {
                "type": "statement",
                "line": "終了です",
                "nextId": -1
            }
        };
        SetDialogueData(val);
    }

    void SetDialogueData(JsVal val)
    {
        list<JsProp> dialogues = val.GetPropertyList();
        for (int i = 0; i < dialogues.Count; i++)
        {
            JsVal val = dialogues[i].GetValue();

            if (val.GetProperty("type").GetStr() == "statement")
            {
                AddDialogue(i, val);
            }
            else
            {
                AddChoiceDialogue(i, val);
            }

        }
    }

    void AddDialogue(int id, JsVal val)
    {
        Dialogue unit = new Dialogue();
        unit.Construct(
            id,
            val.GetProperty("nextId").GetNum(),
            val.GetProperty("line").GetStr()
        );
        IDialogue idialogue = new IDialogue();
        idialogue.Construct(unit, null);
        _IDialogues.Add(idialogue);
    }

    void AddChoiceDialogue(int id, JsVal val)
    {
    
        ChoiceDialogue unit = new ChoiceDialogue();
        list<int> nextIds = new list<int>();
        JsVal ids = val.GetProperty("nextIds");
        for (int i = 0; i < ids.GetPropertyCount(); i++)
        {
            int nextId = int(ids.At(i).GetNum());
            nextIds.Add(nextId);
        }
        string line = val.GetProperty("line").GetStr();
        unit.Construct(id, nextIds, line);

        IDialogue idialogue = new IDialogue();
        idialogue.Construct(null, unit);
        _IDialogues.Add(idialogue);
    }
}
```
### DialogueService
:::note info
**メリット**
- 各実装クラスを知らなくてもよくなっています
- if文の無駄な分岐処理がなくなっています
:::

```c#
class DialogueService
{
    /// <summary>
    /// 全NPCのダイアログのリスト
    /// </summary>
    list<IDialogue> _IDialogues;

    public DialogueService()
    {
        DialogueData data = new DialogueData();
        _IDialogues = data.Value();
    }

    /// <summary>
    /// ダイアログIDからセリフを取得する
    /// </summary>
    public string GetLine(int dialogueId)
    {
        string line = "";
        for (int i = 0; i < _IDialogues.Count(); i++)
        {
            IDialogue idialogue = _IDialogues[i];
            if (idialogue.ID() == dialogueId)
            {
                line = idialogue.Line();
            }
        }
        return line;
    }

    /// <summary>
    /// ダイアログIDからダイアログを取得する
    /// </summary>
    public IDialogue GetDialogue(int dialogueId)
    {
        for (int i = 0; i < _IDialogues.Count(); i++)
        {
            IDialogue idialogue = _IDialogues[i];
            if (idialogue.ID() == dialogueId)
            {
                return idialogue;
            }
        }
        return null;
    }

    /// <summary>
    /// 次のダイアログIDを取得する
    /// </summary>
    public int GetNextDialogueId(int dialogueId)
    {
        IDialogue idialogue = GetDialogue(dialogueId);
        if (idialogue.IsChoiceDialogue())
        {
            return idialogue.AsChoiceDialogue().GetNextDialogueIdByChoice(0);
        }
        return idialogue.AsDialogue().GetNextDialogueId();
    }
}
```