【Unity】asmdef内クラスからどうにかしてasmdef外クラスのメソッドを呼びたい！

```mermaid
flowchart LR
    subgraph asmdef
        asmdef内クラス
    end
    中継クラス
    asmdef外クラス

    中継クラス -->|外部から\nAction登録| asmdef内クラス
    asmdef内クラス -.->|Action\n発火通知| 中継クラス
    中継クラス --> asmdef外クラス
```

# asmdef内クラスからasmdef外クラスのメソッドを呼びたい！

という状況に遭遇することがあります。

たとえば、

- 利用しているサードパーティー製のライブラリがasmdefに対応していない
- 古めの自社コードがasmdefに対応しておらず、すぐに対応させるのも困難

みたいな状況です。

結論から書きます。

asmdef内クラスからasmdef外クラスのメソッドを **直接呼ぶことは不可能です！**

しかし、 **間接的にメソッドを呼ぶことはできます！**



# asmdefがなければ何も気にしなくていい

まず、asmdefがない状況を考えます。

たとえば、ユーザーの身長・体重の値を更新したら、BMIを計算する処理を実装したいとします。

具体的には、以下のように、 `UserProfile` から `BmiUtility` を参照したい！という状況です。

```mermaid
flowchart LR
    UserProfile --> BmiUtility
```

```c#
public class UserProfile
{
    public double Height { get; private set; }
    public double Weight { get; private set; }
    public double Bmi { get; private set; }

    public UserProfile(double height, double weight)
    {
        Height = height;
        Weight = weight;
    }

    public void SetHeight(double height)
    {
        Height = height;
        Bmi = BmiUtility.CalcBmi(Height, Weight);
    }

    public void SetWeight(double weight)
    {
        Weight = weight;
        Bmi = BmiUtility.CalcBmi(Height, Weight);
    }
}
```

```c#
public static class BmiUtility
{
    public static double CalcBmi(double height, double weight)
    {
        // 体重[kg] ÷ (身長[m])^2
        return weight * 10000 / (height * height);
    }
}
```

asmdefがなければ、直接参照することができます！



# asmdef内クラスからasmdef外クラスのメソッドは呼べない！

しかし、たとえば、

- `UserProfile` がasmdef内
- `BmiUtility` がasmdef外

の場合、参照エラーが発生してしまいます！

```mermaid
flowchart LR
    subgraph asmdef
        UserProfile 
    end
    BmiUtility

    UserProfile -->|外部への\n参照はNG| BmiUtility
```

困りました！


# asmdef外からasmdef 内を参照するようにしてみる

発想を逆転させてしまいましょう！

asmdef外に中継クラスを作って、asmdef外からasmdef内を参照するようにしてみるんです！

図にするとこんな感じです！

```mermaid
flowchart LR
    subgraph asmdef
        UserProfile 
    end
    UserBehaviour
    BmiUtility

    UserBehaviour -->|外部からの\n参照はOK| UserProfile 
    UserProfile -.->|通知| UserBehaviour
    UserBehaviour --> BmiUtility
```

まず、以下のように `UserProfile` へ `Action` を導入しましょう！

```c#
using System;

public class UserProfile
{
    public double Height { get; private set; }
    public double Weight { get; private set; }
    public double Bmi { get; private set; }

    public Action OnSetHeight();
    public Action OnSetWeight();

    public UserProfile(double height, double weight)
    {
        Height = height;
        Weight = weight;
    }

    public void SetHeight(double height)
    {
        Height = height;

        // アクションを発火する
        OnSetHeight?.Invoke();
    }

    public void SetWeight(double weight)
    {
        Weight = weight;

        // アクションを発火する
        OnSetWeight?.Invoke();
    }

    public void SetBmi(double bmi)
    {
        Bmi = bmi;
    }
}
```

次に、 `UserBehaviour` というクラスをasmdef外に作って、 以下のように `UserProfile` を参照するようにします。

```c#
using UnityEngine;

public class UserBehaviour : MonoBehaviour
{
    public UserProfile User { get; set; }

    private void Awake()
    {
        User.OnSetHeight += CalcBmi;
        User.OnSetWeight += CalcBmi;
    } 

    private void OnDestroy()
    {
        User.OnSetHeight -= CalcBmi;
        User.OnSetWeight -= CalcBmi;
    } 

    private void CalcBmi()
    {
        var bmi = BmiUtility.CalcBmi(User.Height, User.Weight);
        User.SetBmi(bmi);
    }
}
```

これで、身長・体重が更新されると、 `UserBehaviour.CalcBmi()` が実行されるようになります！

asmdef内から間接的にasmdef外のメソッドを呼ぶことができました！

```c#
public static class BmiUtility
{
    public static double CalcBmi(double height, double weight)
    {
        // 体重[kg] ÷ (身長[m])^2
        return weight * 10000 / (height * height);
    }
}
```

`BmiUtility` は、とくに変更する必要はありません。

`Action` を利用することで、クラスの参照方向を逆転させることができました！

図にまとめると、こんな感じです！

```mermaid
flowchart LR
    subgraph asmdef
        asmdef内クラス
    end
    中継クラス
    asmdef外クラス

    中継クラス -->|外部から\nAction登録| asmdef内クラス
    asmdef内クラス -.->|Action\n発火通知| 中継クラス
    中継クラス --> asmdef外クラス
```


# デメリット1: Actionにメソッドを登録するまでは、メソッドを呼べない

すごく便利ですが、デメリットもあります。

一番のデメリットは、Actionにメソッドが登録されるまでは、アクションを発火させても何も起きないということです。

つまり、

```c#
User.OnSetHeight += CalcBmi;
``` 

が実行されるよりも前に、

```c#
OnSetHeight?.Invoke();
```

が実行されてしまうと、どうしようもないのです。

たとえば、

```c#
public UserProfile(double height, double weight)
```

のコンストラクターが呼ばれた時点では、Actionにメソッドが登録されていないため、コンストラクター内でActionを発火させても、 `BmiUtility.CalcBmi` を実行することはできません。

クラス設計する際に、このあたりの実行順には、かなり気を使う必要があります。



# デメリット2: OnDestroy書くの面倒

今回のサンプルコードだと、 `UserBehaviour` は `MonoBehaviour` を継承しています。

そこで、

```c#
private void Awake()
{
    User.OnSetHeight += CalcBmi;
} 
```

と書いたなら、

```c#
private void OnDestroy()
{
    User.OnSetHeight -= CalcBmi;
} 
```

も同時に書いた方が安全です。

しかし、書かなくても動くので、書き忘れやすいというデメリットがあります。
（もっとスマートな方法ないかな）



# デメリット3: 技術的負債になる可能性がある

もし、リファクタリングがあり、 `BmiUtility` がasmdef内クラスになった場合、 `UserProfile` から直接参照できるようになるはずですが、そのことに気づかなければ以下の図のようになります。

```mermaid
flowchart LR
    subgraph asmdef
        UserProfile 
    end
    UserBehaviour
    subgraph 別のasmdef
        BmiUtility
    end

    UserBehaviour -->|外部からの\nAction登録| UserProfile 
    UserProfile -.->|Action\n発火通知| UserBehaviour
    UserBehaviour --> BmiUtility
```

この状態でも動作はするので、そのまま放置される可能性があります。
そして長い月日が経つと、中継クラスが追加された経緯を知る人がいなくなり、技術的負債となります。

これが3つ目のデメリットです！

もし、この状態に気づけたら、中継クラスをばっさりと廃止しましょう！

```mermaid
flowchart LR
    subgraph asmdef
        UserProfile 
    end
    subgraph 別のasmdef
        BmiUtility
    end

     UserProfile --> BmiUtility
```

すごくシンプルになりましたね！



# さいごに

Actionを使えば、間接的にasmdef内から外のメソッドを呼べるよ！というお話でした！

ご参考になれば幸いです！