【Unity】クリーンアーキテクチャでデッキ編集画面を作りたかった

# 動機
デッキ編集画面はGUIだけで動かせるのでクリーンアーキテクチャを実践できると思った。
クリーンアーキテクチャを調査したかった。

# 具体的な目的
1. 以前、そういう画面を作ったときに、マウス操作で考えていたものを突然ゲームパッドやキーボードにも対応できるようにしなきゃいけなくったときに、すごく辛かったので、クリーンアーキテクチャを使えば、拡張や差し替えが楽になると思った
2. Unityの.asmdefを調査したかった

# 結果
![Movie_001.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304635/40ee7f63-b737-d62e-645d-a7219e59e7d5.gif)

ゲームパッド対応前に力尽きました

# プロジェクト置き場
https://github.com/takenokohal/DeckEditCA

ゲームパッド、キーボード対応をしようとして諦めた残骸も残っています。

# 使用ライブラリ
R3

https://github.com/Cysharp/R3

VContainer

https://vcontainer.hadashikick.jp/ja/

# クラス図
関数は省略

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304635/00e6b689-c1c5-f9b4-f5ec-a5054330193f.png)




# 概要の前に
正しいのか分からないので、参考にするかどうかは自己責任でお願いします…

# サボったこと
クリーンアーキテクチャでは、レイヤーを跨いだデータの渡しかたについて、本来はDTOやViewModelに入れ替えなければならないのですが、サボりました。
ありとあらゆるレイヤーがクラス図内でいうCommonEntityに依存しています。

# 概要
### CommonEntity
カード
```C#
namespace CommonEntity
{
    public class CardEntity
    {
        public string CardKey { get;  }

        public CardEntity(string cardKey)
        {
            CardKey = cardKey;
        }
    }
}
```

## Domain
### Entity
デッキ
```C#
using CommonEntity;
using ObservableCollections;
using R3;

namespace Domain.Entity
{
    public class DeckEntity
    {
        private readonly ObservableList<CardEntity> _cardEntities = new();

        public Observable<CardEntity> ObserveAdd() => _cardEntities
            .ObserveAdd().Select(value => value.Value);

        public Observable<CardEntity> ObserveRemove() => _cardEntities
            .ObserveRemove().Select(value => value.Value);

        public void Add(CardEntity cardEntity)
        {
            _cardEntities.Add(cardEntity);
        }

        public void Remove(CardEntity cardEntity)
        {
            _cardEntities.Remove(cardEntity);
        }

        private readonly ReactiveProperty<CardEntity> _currentFocusedCard = new();

        public CardEntity CurrentFocusedCard
        {
            get => _currentFocusedCard.Value;
            set => _currentFocusedCard.Value = value;
        }

        public Observable<CardEntity> ObserveCurrentFocusedCard() => _currentFocusedCard;
    }
}
```

カードプール
```C#
using System.Collections.Generic;
using CommonEntity;
using R3;

namespace Domain.Entity
{
    public class CardPoolEntity
    {
        private List<CardEntity> _cardList;
        public IReadOnlyList<CardEntity> CardList => _cardList;

        public void Reset(IEnumerable<CardEntity> cardEntities)
        {
            _cardList = new List<CardEntity>(cardEntities);
            _onReset.OnNext(Unit.Default);
        }

        private readonly Subject<Unit> _onReset = new();
        public Observable<Unit> ObserveReset() => _onReset;
    }
}
```

### UseCase
デッキにカードを追加
```C#
using CommonEntity;
using Domain.Entity;
using R3;

namespace Domain.UseCase
{
    public class AddUseCase
    {
        private readonly DeckEntity _deckEntity;

        public AddUseCase(DeckEntity deckEntity)
        {
            _deckEntity = deckEntity;
        }
        public void Add(CardEntity cardEntity) => _deckEntity.Add(cardEntity);
        public Observable<CardEntity> ObserveAdd() => _deckEntity.ObserveAdd();
    }
}
```

デッキからカードを削除
```C#
using CommonEntity;
using Domain.Entity;
using R3;

namespace Domain.UseCase
{
    public class RemoveUseCase
    {
        private readonly DeckEntity _deckEntity;

        public RemoveUseCase(DeckEntity deckEntity)
        {
            _deckEntity = deckEntity;
        }
        public void Remove(CardEntity cardEntity) => _deckEntity.Remove(cardEntity);

        public Observable<CardEntity> ObserveRemove() =>
            _deckEntity.ObserveRemove();
    }
}
```

カードプールの更新
```C#
using System.Collections.Generic;
using CommonEntity;
using Domain.Entity;
using R3;

namespace Domain.UseCase
{
    public class ResetCardPool
    {
        private readonly CardPoolEntity _cardPoolEntity;
        private readonly ICardPoolGateway _cardPoolGateway;

        public ResetCardPool(CardPoolEntity cardPoolEntity, ICardPoolGateway cardPoolGateway)
        {
            _cardPoolEntity = cardPoolEntity;
            _cardPoolGateway = cardPoolGateway;
        }


        public void Reset() => _cardPoolEntity.Reset(_cardPoolGateway.GetCardPool());

        public Observable<IReadOnlyList<CardEntity>> ObserveReset() =>
            _cardPoolEntity.ObserveReset().Select(_ => _cardPoolEntity.CardList);
    }
}
```

## Adapter
### Presenter
カード追加処理の、プレゼンターとビューのインターフェース
```C#
using Domain.UseCase;
using R3;
using VContainer.Unity;

namespace Adapter.Presenter
{
    public class AddPresenter : IInitializable
    {
        private readonly IAddView _addView;
        private readonly AddUseCase _addUseCase;

        public AddPresenter(IAddView addView, AddUseCase addUseCase)
        {
            _addView = addView;
            _addUseCase = addUseCase;
        }
        
        public void Initialize()
        {
            _addUseCase.ObserveAdd().Subscribe(value => _addView.OnAdd(value));
        }
    }
}
```

```C#
using CommonEntity;

namespace Adapter.Presenter
{
    public interface IAddView
    {
        public void OnAdd(CardEntity cardEntity);
    }
}
```


カード削除処理の、プレゼンターとビューのインターフェース
```C#
using Domain.UseCase;
using R3;
using VContainer.Unity;

namespace Adapter.Presenter
{
    public class RemovePresenter : IInitializable
    {
        private readonly RemoveUseCase _removeUseCase;
        private readonly IRemoveView _removeView;

        public RemovePresenter(RemoveUseCase removeUseCase, IRemoveView removeView)
        {
            _removeUseCase = removeUseCase;
            _removeView = removeView;
        }

        public void Initialize()
        {
            _removeUseCase.ObserveRemove().Subscribe(value => _removeView.OnRemove(value));
        }
    }
}
```

```C#
using CommonEntity;

namespace Adapter.Presenter
{
    public interface IRemoveView
    {
        public void OnRemove(CardEntity cardEntity);
    }
}
```

カードプールの、プレゼンターとビューのインターフェース
```C#
using Domain.UseCase;
using R3;
using VContainer.Unity;

namespace Adapter.Presenter
{
    public class CardPoolPresenter : IInitializable
    {
        private readonly ResetCardPool _resetCardPool;
        private readonly ICardPoolView _cardPoolView;

        public CardPoolPresenter(ResetCardPool resetCardPool, ICardPoolView cardPoolView)
        {
            _resetCardPool = resetCardPool;
            _cardPoolView = cardPoolView;
        }

        public void Initialize()
        {
            _resetCardPool.ObserveReset().Subscribe(value => _cardPoolView.OnReset(value));
        }
    }
}
```

```C#
using System.Collections.Generic;
using CommonEntity;

namespace Adapter.Presenter
{
    public interface ICardPoolView
    {
        public void OnReset(IReadOnlyList<CardEntity> cardEntities);
    }
}
```

### Controller
```C#
using CommonEntity;
using Domain.UseCase;

namespace Adapter.Controller
{
    public class AddController
    {
        private readonly AddUseCase _addUseCase;

        public AddController(AddUseCase addUseCase)
        {
            _addUseCase = addUseCase;
        }

        public void Add(CardEntity cardEntity) => _addUseCase.Add(cardEntity);
    }
}
```

```C#
using CommonEntity;
using Domain.UseCase;

namespace Adapter.Controller
{
    public class RemoveController
    {
        private readonly RemoveUseCase _removeUseCase;

        public RemoveController(RemoveUseCase removeUseCase)
        {
            _removeUseCase = removeUseCase;
        }

        public void Remove(CardEntity cardEntity)
        {
            _removeUseCase.Remove(cardEntity);
        }
    }
}
```

### Gateway
データベースとの橋渡し
```C#
using System.Collections.Generic;
using CommonEntity;
using Domain.UseCase;

namespace Adapter.Gateway
{
    public class CardPoolGateway : ICardPoolGateway
    {
        private readonly ICardDatabase _cardDatabase;

        public CardPoolGateway(ICardDatabase cardDatabase)
        {
            _cardDatabase = cardDatabase;
        }

        public IReadOnlyList<CardEntity> GetCardPool()
        {
            return _cardDatabase.GetCardList();
        }
    }
}
```

```C#
using System.Collections.Generic;
using CommonEntity;

namespace Adapter.Gateway
{
    public interface ICardDatabase
    {
        public IReadOnlyList<CardEntity> GetCardList();
    }
}
```


## Detail
### View
デッキ
```C#
using System.Collections.Generic;
using System.Linq;
using Adapter.Presenter;
using CommonEntity;
using UnityEngine;
using VContainer;

namespace Detail.View
{
    public class DeckView : MonoBehaviour, IAddView, IRemoveView
    {
        [Inject] private readonly DeckCardViewFactory _deckCardViewFactory;

        private readonly List<DeckCardView> _deckCardViews = new();
        public IReadOnlyList<DeckCardView> DeckCardViews => _deckCardViews;

        public void OnAdd(CardEntity cardEntity)
        {
            var instance = _deckCardViewFactory.Create(cardEntity);
            _deckCardViews.Add(instance);
        }

        public void OnRemove(CardEntity cardEntity)
        {
            var instance = _deckCardViews.First(value => value.CardEntity == cardEntity);
            _deckCardViews.Remove(instance);
            Destroy(instance.gameObject);
        }
    }
}
```

```C#
using Adapter.Controller;
using Adapter.Presenter;
using CommonEntity;
using UnityEngine;
using VContainer;

namespace Detail.View
{
    public class DeckCardViewFactory : MonoBehaviour
    {
        [SerializeField] private DeckCardView deckCardViewPrefab;

        [SerializeField] private Transform parent;


        [Inject] private readonly RemoveController _removePresenter;

        public DeckCardView Create(CardEntity cardEntity)
        {
            var instance = Instantiate(deckCardViewPrefab, parent);
            instance.Construct(cardEntity, _removePresenter);


            return instance;
        }
    }
}
```

```C#
using Adapter.Controller;
using Adapter.Presenter;
using CommonEntity;
using R3;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

namespace Detail.View
{
    public class DeckCardView : MonoBehaviour
    {
        [SerializeField] private Button button;
        [SerializeField] private TMP_Text text;


        public CardEntity CardEntity { get; private set; }
        private RemoveController _removeController;

        public void Construct(CardEntity cardEntity, RemoveController removeController)
        {
            CardEntity = cardEntity;
            _removeController = removeController;

            Init();
        }

        private void Init()
        {
            text.text = CardEntity.CardKey;

            button.OnClickAsObservable().Subscribe(_ => _removeController.Remove(CardEntity));
        }
    }
}
```

カードプール
```C#
using System.Collections.Generic;
using Adapter.Presenter;
using CommonEntity;
using UnityEngine;
using VContainer;

namespace Detail.View
{
    public class CardPoolView : MonoBehaviour, ICardPoolView
    {
        [Inject] private readonly CardPoolCardViewFactory _cardViewFactory;

        private readonly List<CardPoolCardView> _instances = new();

        public void OnReset(IReadOnlyList<CardEntity> cardEntities)
        {
            foreach (var cardPoolCardView in _instances)
            {
                Destroy(cardPoolCardView.gameObject);
            }

            _instances.Clear();

            foreach (var cardEntity in cardEntities)
            {
                var instance = _cardViewFactory.Create(cardEntity);
                _instances.Add(instance);
            }
        }
    }
}
```

```C#
using Adapter.Controller;
using Adapter.Presenter;
using CommonEntity;
using UnityEngine;
using VContainer;

namespace Detail.View
{
    public class CardPoolCardViewFactory : MonoBehaviour
    {
        [SerializeField] private CardPoolCardView cardPrefab;
        [SerializeField] private Transform parent;

        [Inject] private readonly AddController _addController;

        public CardPoolCardView Create(CardEntity cardEntity)
        {
            var instance = Instantiate(cardPrefab, parent);
            instance.Construct(cardEntity, _addController);
            return instance;
        }
    }
}
```

```C#
using Adapter.Controller;
using Adapter.Presenter;
using CommonEntity;
using R3;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

namespace Detail.View
{
    public class CardPoolCardView : MonoBehaviour
    {
        [SerializeField] private Button button;
        [SerializeField] private TMP_Text text;
        
        private CardEntity _cardEntity;
        private AddController _addController;

        public void Construct(CardEntity cardEntity, AddController addController)
        {
            _cardEntity = cardEntity;
            _addController = addController;
            
            Init();
        }

        private void Init()
        {
            button.OnClickAsObservable().Subscribe(_ => _addController.Add(new CardEntity(_cardEntity.CardKey)));
            text.text = _cardEntity.CardKey;
        }
    }
}
```

### Database
```C#
using System.Collections.Generic;
using System.Linq;
using Adapter.Gateway;
using CommonEntity;
using UnityEngine;

namespace Detail.DB
{
    [CreateAssetMenu(menuName = "Create CardPoolDatabase", fileName = "CardPoolDatabase", order = 0)]
    public class CardPoolDatabase : ScriptableObject, ICardDatabase
    {
        [SerializeField] private List<CardData> cardData;


        public IReadOnlyList<CardEntity> GetCardList()
        {
            return cardData.Select(value => new CardEntity(value.cardKey)).ToList();
        }
    }
}
```

```C#
using System;

namespace Detail.DB
{
    [Serializable]
    public class CardData
    {
        public string cardKey;
    }
}
```

# 意識したこと、思ったこととか
Unityや外部ライブラリに依存する層は最下層（Detail層）だけにした。
このとき、それより上の層の.asmdef操作が比較的楽だった。
Detail層の.asmdef操作が面倒だったが、この層はガッツリゲームエンジンに依存させているので、.asmdefを作らなくても良いかもと思った。

Entity、UseCase、Controllerの働きの違いを明確に定義することができなかった…
依存逆転の法則が守れているなら、レイヤーは少なくても良いんじゃないかなと思った。

依存逆転の法則を守るときに、Observableにするかinterfaceにするか迷った。
全部interfaceにすると、どこかで循環参照が生まれるはず。
全部Observableにすると、たしか処理順とかが変になっちゃった気がする…

.asmdefの調査は楽しかった。今度からどんどんやりたい。

デッキ編集画面の答えが分からない…
シンプルに「動的にボタンを生成して、ボタンを押すとそのボタンが削除される」の実装の答えが分からない…