CleanArchitectureでasmdefをガチった話(Unity)

# 概要

本記事ではClean Architecture定義したレイヤをUnityで実装する際、Assembly Definitionを切るとレイヤ同士の依存関係を明確にできていい感じになるよ、という話をします。

# Clean Architecture

CleanArchitectureは例の図の内側にアプリケーションロジックを閉じ込め、変更されがちなフレームワークやデータベースを詳細として捉え、それらをアプリケーションロジックの外側に追いやり中間層を介してアプリケーションロジックから詳細を参照することで、詳細にとらわれないアプリケーション開発を行うアーキテクチャです。

<img width="480" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274974/9626af26-3117-51b2-90b6-90d61cd85627.png">

* [The Clean Architecture @The Clean Code Blog](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)

Unityでの採用例には [CAFU](https://github.com/umm/cafu_core) というフレームワークがあるので、ここから調べるのが良いでしょう。
作成者(日本人)の登壇資料も参考になります。

* [Clean Architecture for Unity @unity LearningMaterials](https://learning.unity3d.jp/4021/)

# Assembly Definition

AssemblyDefinitionはUnityの機能の１つで、フォルダ内に `.asmdef` ファイルを配置することでそのフォルダ以下を別dll(=別アセンブリ?)として定義することのできる機能です。

<img width="562" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274974/fe53445d-e03c-df74-f1d3-fab6832090d1.png">

* [Assembly Definition Filesという神機能 @テラシュールブログ](http://tsubakit1.hateblo.jp/entry/2018/01/18/212834)

`.asmdef` をフォルダに作成するとその階層以下にあるScriptは同階層以下のScriptしか参照できなくなります。外のScriptを参照したい場合は該当Scriptの属する `.asmdef` への参照を自分の `.asmdef` に設定してやる必要があります。


よくテストを実装しようとしてテスト用の `.asmdef` を切ったは良いが、自プロジェクトや3rdのPluginが `.asmdef` を切ってなくて仕方なく追加するという人が多い印象を持ってます。

## Clean Architecture x Assembly Definitionのメリット

そんな便利だがネガティブな印象を持たれてそうなAssembly Definitionですが、Clean Architectureで定義したレイヤの関係性を制限させたい場合にも効果を発揮します。
(※別にClean Architectureに限る訳ではないですが)


### ディレクトリ分けの方針になる

CleanArchitectureではファイルが増えがちです。

* ex. IHogePresenter.cs/HogePresenter.cs, IHogeView.cs/HogeView.cs, IHogeRepository.cs/HogeRepository.cs ...

なんとなくでディレクトリ(フォルダ)を分けていると、開発が進むにつれて不整合が出てきて管理しきれなくなる可能性があります。そこで、 `.asmdef` を切る方針でいれば dll 分けを考慮したディレクトリ構成になるので、基本的には正しいディレクトリ構成になると期待できます。

### レイヤ間(またはモジュール同士)の関係性を制限できる

`.asmdef` を一切切らない場合、プロジェクト内のScriptはどのScriptにも参照できます。


仮に UseCase からUI系の処理を行う場合は `IPresenter` を経由して `View` を操作しようと方針を決めたとします。しかし、開発者の理解不足や凡ミスにより UseCase に直接 `View` を渡しちゃったというような方針と異なる実装が行われてしまうケースが当然あり得ます。(もしくは都合がいいから `View` から `Repository` にアクセスしちゃうケースとか)

レビューで気付けばいいですが、ビルドも動作も想定通りに行われる場合だと方針違いにレビューで気づくのは結構困難でしょう。

そこで、設定のレイヤやモジュールごとに `.asmdef` を定義し、それらの関係を大元で決めてやれば、想定外のレイヤやモジュール同士の依存関係をシャットアウトすることができます。
レビューでも `.asmdef` に変更が入ってないかをチェックすればOKなのでお手軽です。

# 分け方の実例

ソースコードはGitHubにあげていますので、必要あれはご覧ください。

* https://github.com/naninunenoy/UnityCleanArchitectureExample

## Clean Architecture におけるモジュール

自分の考えるUnityアプリ(ゲームじゃない)を作る上でのClean Architectureの構成は以下のような図になります。

<img width="562" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274974/a7a8b2e5-37f4-2936-4291-9917fbba17f0.png">

各モジュールについて説明します。

### <font color="Salmon">Domain</font>

#### UseCase
アプリケーションロジックをここに書きます。
アプリケーションでやりたいことを `IPresetner` と `IRepository` を使って実現します。

```c#:例
public class UseCase : IUseCase {
  readonly IPresenter presenter;
  readonly IRepository repository;
  public UseCase(IPresenter presenter,
                 IRepository repository) {
    this.presenter = presenter;
    this.repository = repository;
  }
  void IUseCase.Begin() {
    // presenter経由でボタンが押されたイベントを取得
    // repository経由で回数を保存みたいな
  } 
  void IUseCase.Finish() {
    // 終了処理
  }
}
```

#### IPresenter/IRepository
UseCaseを満たすため、UIやDataに対する処理を定義します。UseCase(やりたいこと)があってこいつらが形作られるので、これらはDomainの存在になります。

```c#:例
public interface IPresenter {
  IObservable<Unit> ClickObs {get;}
  void SetText(string text);
}
public interface IRepository {
  void SetCount(int count);
  int GetCount();
}
```

UIの処理とはいえ `UnityEngine.UI` への処理はまだ不要です。
`Button` のイベントを取得するために UniRx を使っています。また、DataのI/Oを非同期で行いたければ UniTask などを導入すると良いでしょう。


### <font color="SpringGreen">Adapter</font>

実装の詳細をアプリケーションロジックをつなぐ中間層です。

#### Presenter/Repository
こいつらもまだ `UnityEngine.UI` とかデータの保存先が `PlayerPrefs` なのか Firebase などの mBaaS なのかも知りません。

```c#:例
//using UnityEngine.UI; 不要ではある
public class Presenter : IPresenter {
  readonly IView view;
  public Presenter(IView view) {
    this.view = view;
  }
  public IObservable<Unit> ClickObs => view.Button.OnClickObservable();
  public void SetText(string text) {
    view.Text.text = text; // しかしTextがここで出てくる
  }
}

public class Repository : IRepository {
  readonly IDataStore datastore;
  public Repository(IDataStore dataStore) {
    this.datastore = dataStore;
  }
  public void SetCount(int count) {
    dataStore.SaceCount(count);
  }
  public int GetCount() {
    return dataStore.LoadCount();
  }
}
```

#### IView/IDataStore

中間層と詳細の橋渡しを行う定義です。

```c#:例
public interface IView {
  Button Button {get;}
  Text Text {get;}
}

public interface IDataStore {
  void SaveCount(int count);
  int LoadCount();
}
```

### <font color="SteelBlue">Detail</font>
詳細の実装を行う箇所です。思う存分フレームワークに依存させてください。
ここが無くなったり更新されたとしても <font color="Salmon">Domain</font> のアプリケーションロジックには一切変更が必要ないというのがミソです。

#### View/DataStore

```c#:例
public class View : MonoBehaviour, IView {
  [SerializeField] Button button = default;
  [SerializeField] Text text = default;
  public Button Button => button;
  public Text Text => text;
}
public class DataStore : IDataStore {
  public void SaveCount(int count) {
    PlayerPrefs.SetInt("count", count);
  }
  public int LoadCount() {
    return PlayerPrefs.GetInt("count", 0);
  }
}
```

ここで初めて `MonoBehaviour` とか `PlayerPrefs` とかUnityの都合が出てきます。

### Main

`Main` はUnityのシーンに乗せて、`UseCase` を作成/実行/破棄する役割を持たせます。
`UseCase` を作成するための材料になる(コンストラクタの引数になる) `IPresenter` や `IRepository` は `[Zenject.Inject]` から受け取ると良いでしょう。

```c#:例
public class Main : MonoBehaviour {
  IUseCase usecase;
  [Inject] 
  Construct(IPresenter presenter, IRepository repository) {
    // usecaseの作成
    usecase = new UseCase(presenter, repository);
  }
  void Awake() {
    // usecaseの実行
    usecase.Begin();
  }
  void OnDestroy() {
    // usecaseの破棄
    usecase.Finish();
  }
}
```

`[Zenject.Inject]` でinterfaceの実装を受け取るためには別途DIの設定を記述する必要があります。

### Installer

前述のDIの設定を記述します。こいつには `Zenject.MonoInstaller` を継承させます。

```c#:例
public class Installer : MonoInstaller {
 [SerializeField] View view;
 [SerializeField] DataStore dataStore;
 public override void InstallBindings() {
  Container
   .Bind<IPresenter>()
   .FromInstance(new Presenter(view))
   .AsCached();
  Container
   .Bind<IRepository>()
   .FromInstance(new Repository(dataStore))
   .AsCached();
 }
}
```

こいつはDIのため詳細のクラス定義を知っている必要があります。

## `.asmdef` 構成

お待たせしました。やっと本題です。

どのモジュールがどのモジュールに参照できるか(すべきか)を考えて `.asmdef` 分けを行った結果次のようになりました。黄色の四角1つ1つが `.asmdef` です。

<img width="641" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274974/e787a643-7315-3543-ad0a-1db18a10cae6.png">

なんと1つのプロジェクトで **11個** もの `.asmdef` を定義することになりました。
(テスト用 `.asmdef` は含めず)

各 `.asmdef` がどれにアクセスするかを説明します。

### UniRx/UniTask/Zenjectについて

こいつらのライブラリの役割について補足しておきます。
「ライブラリ(=フレームワーク)は詳細に追いやってアプリケーションロジックからは参照させるべきでない」
みたいな説明を先でしましたが、こいつらは別です。各モジュール同士のやりとりにもこの3つは使われているので、アーキテクチャそのものがこの3つのライブラリに依存している形になります。(作るものによっては `UnityEngine` よりこいつらのが重要かもしれません)

もし3rdライブラリへの依存を避けたいなら、`event Action` とか `System.Threading.Tasks.Task` を使うようにしてください。DIはサービスロケータを自作するとかでしょうか。

### UseCase

アクセスするのは同じ <font color="Salmon">Domain</font> の中のinterfaceだけです。必要に応じて共通のデータ型定義やロジックのクラスへアクセスしてください。

<img width="641" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274974/19e7990b-caff-c7ef-425a-0b59bfeb952e.png">

### IPresenter/IRepository

こいつらがアクセスする必要があるのは共通データ型くらいでしょう。詳細は知る必要ありません。

<img width="649" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274974/35fadae7-dee6-a51e-ab97-8c68385501f9.png">

### Presenter/Repository(+IView/IDataStore)

inrefaceを実装したクラスなので、interface定義を知っている必要があります。

<img width="641" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274974/006ea8d9-d577-74ff-582c-1a25a2af2a1f.png">

`IView` と `IDataStore` も一緒にしましたが、必要だと思うなら `.asmdef` を追加しても良いでしょう。

<font color="SpringGreen">Adapter</font> から <font color="Salmon">Domain</font> に向く矢印が上を向いているのが CleanArchitecture 的なポイントの１つでもあります。

### View/DataStore

こいつらもinrefaceを実装したクラスなので、interface定義を知っている必要があります。

<img width="641" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274974/00d3fbe4-e1b5-6e64-7bd6-9d999b3c580c.png">

ここでも <font color="SteelBlue">Detail</font> から<font color="SpringGreen">Adapter</font> への矢印が上を向いていることに注目してください。

別のUIフレームワークやmBaaSのライブラリに依存する場合はここの `.asmdef` のみを編集すれば良く、内側の <font color="SpringGreen">Adapter</font> や <font color="Salmon">Domain</font> はここの変更による影響を一切受けないということがポイントです。

### Main

シーンで実行され `UseCase` を作成する役割を担います。

<img width="641" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274974/efff02b5-6385-6f50-48b6-1542f56c168e.png">

`UseCase` とそれの材料になる interface の定義のみを知っていれば良いです。

### Installer

`Main` が `UseCase` を作成するための材料を事前に準備(DI)します。役割上知る範囲がかなり広大です。

<img width="641" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274974/2e73a8d7-d4c1-7b08-8eff-263eb4b1bce2.png">

`Installer` は `Main` を直接知りません。(`Main` も `Installer` を知りません。)
Zenject経由で `Installer` が行ったDIの設定を `Main` が勝手に受け取っているという表現が正しいです。

# まとめ

UnityでClean Architectureを導入しようというなら、Assembly Definitionを使ってこんな感じに分けるといいよという話でした。

感じたメリットとしては

* 自然とディレクトリも分けられる
* 関心の分離にも繋がる
* Intellisenseの候補が絞られる
    * 似た名前のファイルが多くなるので結構汚染される
* レビュー時には `.asmdef` にさえ気を配っておけば良い

注意点としては

* asmdefの管理者が必要にある
* AssetBundleに罠がある(らしい)
* `.csproj`が増えて数にビビる
    * 実害はないですがビックリします

でしょうか。
`.asmdef` も増えすぎると、可視化や不要な参照を含んでないかのバリデーションツールも欲しくなりそうです。