LoginSignup
1
1

More than 3 years have passed since last update.

Unity Assembly definition Files を利用したより強固なレイヤ定義

Posted at

はじめに

よく実装は「疎結合にすべき」といわれます。
そのためにレイヤードアーキテクチャに腐敗防止層を設けたり、依存性逆転の原則を適用したりして、3rdパーティのライブラリの変更から自身のコードを守ることがよく行われています。

image.png

Assembly definition

Unityでは Assembly definitionによりアセンブリ空間を定義でき、ディレクトリ構成を基にアセンブリ空間を分離することができます。

レイヤ定義を namespace 任せにするだけでなく、アセンブリ空間さえ分離してやることでより他の開発者にレイヤ構成を遵守させることができそうです。

アセンブリ空間分離のお試しプロジェクトを作成しました。

image.png

以下で、レイヤ分離の意図を解説します。

詳細は知らない

Main はインタフェースの IDependencee を所持しており処理を行います。また、MonoBehaviour でもあるのでUnityから Start() が実行され、 .DoSomething() が実行されます。

public class Main : MonoBehaviour
{
    [Inject] IDependencee dependencee;

    void Start()
    {
        dependencee.DoSomething();
        Debug.Log(dependencee.GetCommonObject().value);
    }
}

Main が知っているのは interface の IDependencee のみであり、実装の詳細である Dependencee に関する知識はありません。これは Dependencee の実装が今後変更された場合も Main には影響が及ばないことを意味しています。

さらに言えば、 Dependencer.asmdefDependenceeImplement.asmdef を参照しないので、Main.cs の実装内では Dependencee クラスのインスタンスをnewすることすらできません。

Dependencer.asmdef が参照するのはいわゆる腐敗防止層である DependenceeAbstruct.asmdef になります。
ここには振る舞いを表すinterfaceのみが定義されています。

namespace AsmdefDependencyPattern.DependenceeAbstruct
{
    public interface IDependencee
    {
        void DoSomething();
        Common GetCommonObject();
    }
}

もちろん DependenceeImplement.asmdef は interface を実装するために、DependenceeAbstruct.asmdef を参照します。
(上の図にはasmdef参照の矢印は含まれていません)

namespace AsmdefDependencyPattern.DependenceeImplement
{
    public class Dependencee : DependenceeAbstruct.IDependencee
    {
        public void DoSomething()
        {
            UnityEngine.Debug.Log("Dependencee.DoSomething()");
        }

        public Common GetCommonObject()
        {
            return new Common {value = 999};
        }
    }
}

Dependency Injection

さて、では MainDependenceeクラスのインスタンスを渡しているのは何者でしょうか?

image.png

Dependency Injection(DI)によってこの問題を解決しています。

Unityではおなじみの Zenject というDIフレームワークを使いました。
IDependencee が要求されたときに、Dependencee のインスタンスが渡されるように設定を行います。

public class DependenceIntermediary : Zenject.MonoInstaller
{
    public override void InstallBindings()
    {
        Container.Bind<IDependencee>().To<Dependencee>().AsTransient();
    }
}

DependenceIntermediary.asmdef は依存性注入のため DependenceeAbstruct.asmdefDependenceeImplement.asmdef の両方を参照します。依存性注入を行う特性上、多くの asmdef を参照するようになると思います。

そしてシーン上に Main , DependenceIntermediary , Zenject.SceneContext を配置することで、SceneContext により IDependenceeDependencee の間の依存関係が解決され、Main.Awake()Main.Start() が実行される時には Dependencee のインスタンスがフィールドインジェクション経由で取得できる訳です。

共通のクラス定義

場合によっては各レイヤを横断するクラスの定義や定数定義が必要になると思われます。
共通の定義は すべての asmdef から参照される必要があるので、これまでの .asmdef に追加するのでなく、新たに共通定義用の asmdef を作成する必要があります。

仮に、 Dependencer.asmdef に共通定義のクラスを作成した場合、DependenceeAbstruct.asmdef から Dependencer.asmdef を参照する必要が出てきます。Dependencer.asmdef は既に DependenceeAbstruct.asmdef を参照しているので、この場合循環参照が発生してしまうので破綻してしまいます。(Assembly definitionでは循環参照を設定することはできません)

新たに作成した Common.asmdef は他の asmdef を参照せず、逆に他3つの asmdef から参照される形になります。Common.asmdef 内のクラスが変更された場合、広範囲の asmdef に影響がでる形になります。

おわり

UnityのAssembly definitionは簡単に定義出来て強力な効果を発揮するのでUnityを使うなら勉強しておいて損は無いはずです。実際私は asmdef のレイヤ定義の過程で依存性逆転の理解がかなりできました。

ちなみに、Dependencer/Dependenceeは造語です。reviewer/revieweeのように、

  • Dependencer: 依存する人(利用側)
  • Dependencee: 依存される側(利用される側)

を意図しています。

asmdefによるレイヤ分けに興味が出てきた方は是非こちらもご覧ください

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1