【Unity】クリーンアーキテクチャでデッキ編集画面リベンジ


https://qiita.com/takenokohal/items/c76b40158e2309be93f4

前回のこちらがあまりいい結果でなかったのでリベンジした。
画面などは前回と同じ

# 参考
https://qiita.com/toRisouP/items/e5b312af53c40e1f4a80

# 使用ライブラリ
前回と同じ

R3

https://github.com/Cysharp/R3

VContainer

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

# プロジェクト置き場

https://github.com/takenokohal/DeckEditCA2

# クラス図

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304635/e8f7ed08-a4f5-3cf8-f926-d39ab5198748.png)

# 前回と違うところ
上下の層の数を減らした。
Domain層内を横並びにした。
UseCaseを通り過ぎてそのままCore層（前回でいうEntity層）を直接触れるようになった。
仲介層をほとんど消した。

# 変更理由
Observerパターンを使っている場合、Domain層の依存関係の逆転法則はほとんど解消されるため、仲介層を増やす意味がほとんどない。
UseCaseの役割を「ただの便利メソッドの集合体」「ただのCore層の拡張」と解釈したので、Core層と横並びにし、UseCaseを使わなくてもCore層を触れるように。

# 概要

## Domain

### Core
デッキ内のカード
```C#
namespace DeckEdit.Domain.Core
{
    public class DeckCard
    {
        public DeckCard(string key)
        {
            Key = key;
        }

        public string Key { get; }
    }
}
```

デッキ
```C#
using ObservableCollections;

namespace DeckEdit.Domain.Core
{
    public class Deck
    {
        private readonly ObservableList<DeckCard> _deckCards = new();
        public void Add(DeckCard deckCard) => _deckCards.Add(deckCard);
        public void Remove(DeckCard deckCard) => _deckCards.Remove(deckCard);
        public IReadOnlyObservableList<DeckCard> DeckCards => _deckCards;
    }
}
```

カードプール内のカード
```C#
namespace DeckEdit.Domain.Core
{
    public class CardPoolCard
    {
        public CardPoolCard(string key)
        {
            Key = key;
        }

        public string Key { get; }
    }
}
```

カードプール
もともとstringのリストだったが、Wrapperしたほうが今後便利そうだったのでCardPoolCardのリストに。
```C#
using System.Collections.Generic;
using R3;

namespace DeckEdit.Domain.Core
{
    public class CardPool
    {
        private readonly List<CardPoolCard> _cardList = new();
        public IReadOnlyList<CardPoolCard> CardList => _cardList;

        public void Reset(IEnumerable<CardPoolCard> next)
        {
            _cardList.Clear();
            _cardList.AddRange(next);
            _onReset.OnNext(default);
        }

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

### UseCase

```C#
using System.Linq;
using DeckEdit.Domain.Core;

namespace DeckEdit.Domain.UseCases
{
    public class CardPoolReset
    {
        private readonly CardPool _cardPool;
        private readonly ICardListLoader _cardListLoader;

        public CardPoolReset(CardPool cardPool, ICardListLoader cardListLoader)
        {
            _cardPool = cardPool;
            _cardListLoader = cardListLoader;
        }

        public void Reset()
        {
            var list = _cardListLoader.LoadCardList();
            _cardPool.Reset(list.Select(value => new CardPoolCard(value)));
        }
    }
}
```

```C#

using System.Collections.Generic;

namespace DeckEdit.Domain.UseCases
{
    public interface ICardListLoader
    {
        public IEnumerable<string> LoadCardList();
    }
}
```

カードプールを1アクションで作り直せるようにUseCaseに。

カードプールをロードするインターフェース。
データベースから持ってくるか、ソートするか、フィルタリングするか、などは移譲先に書く。

### UIs
```C#
using DeckEdit.Domain.Core;
using R3;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

namespace DeckEdit.UIs
{
    public class CardPoolButtonPresenter : MonoBehaviour
    {
        [SerializeField] private Button button;

        [SerializeField] private TMP_Text text;

        private string _myKey;
        private Deck _deck;

        public void SetUp(string myKey, Deck deck)
        {
            _myKey = myKey;
            _deck = deck;
        }

        private void Start()
        {
            text.text = _myKey;
            button.OnClickAsObservable().Subscribe(_ => _deck.Add(new DeckCard(_myKey)));
        }
    }
}
```

```C#
using DeckEdit.Domain;
using DeckEdit.Domain.Core;
using R3;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

namespace DeckEdit.UIs
{
    public class DeckCardButtonPresenter : MonoBehaviour
    {
        private Deck _deck;

        [SerializeField] private Button button;
        [SerializeField] private TMP_Text text;
        

        private DeckCard _deckCard;

        public void SetUp(DeckCard deckCard, Deck deck)
        {
            _deckCard = deckCard;
            _deck = deck;
        }

        private void Start()
        {
            button.OnClickAsObservable().Subscribe(_ => _deck.Remove(_deckCard)).AddTo(this);

            text.text = _deckCard.Key;
        }
    }
}
```

どちらも動的に増えるボタン。
カードプール内のカードをクリックしたらデッキにそのカードを追加。
デッキ内のカードをクリックしたらそのカードを削除。
InputとViewが同居しているけど、これは良いのかなぁ…？

#### Binder
```C#
using System.Collections.Generic;
using DeckEdit.Domain;
using DeckEdit.Domain.Core;
using R3;
using UnityEngine;
using VContainer;

namespace DeckEdit.UIs.Binder
{
    public class CardPoolButtonBinder : MonoBehaviour
    {
        [Inject] private readonly CardPool _cardPool;
        [Inject] private readonly Deck _deck;

        [SerializeField] private CardPoolButtonPresenter buttonPrefab;
        
        private readonly List<CardPoolButtonPresenter> _instances = new();

        private void Start()
        {
            _cardPool.OnReset.Subscribe(_ => OnReset()).AddTo(this);
        }

        private void OnReset()
        {
            foreach (var cardPoolButtonPresenter in _instances)
            {
                Destroy(cardPoolButtonPresenter.gameObject);
            }

            var list = _cardPool.CardList;
            foreach (var cardPoolCard in list)
            {
                var instance = Instantiate(buttonPrefab, transform);
                instance.SetUp(cardPoolCard.Key, _deck);
                _instances.Add(instance);
            }
        }
    }
}
```
```C#
using System.Collections.Generic;
using DeckEdit.Domain;
using DeckEdit.Domain.Core;
using ObservableCollections;
using R3;
using UnityEngine;
using VContainer;

namespace DeckEdit.UIs.Binder
{
    public class DeckCardButtonBinder : MonoBehaviour
    {
        [Inject] private readonly Deck _deck;

        [SerializeField] private DeckCardButtonPresenter deckCardButtonPrefab;

        private readonly Dictionary<DeckCard, DeckCardButtonPresenter> _instances = new();

        private void Start()
        {
            _deck.DeckCards.ObserveAdd()
                .Subscribe(value => OnAdd(value.Value)).AddTo(this);

            _deck.DeckCards.ObserveRemove()
                .Subscribe(value => OnRemove(value.Value)).AddTo(this);
        }

        private void OnAdd(DeckCard deckCard)
        {
            var instance = Instantiate(deckCardButtonPrefab, transform);
            instance.SetUp(deckCard, _deck);

            _instances.Add(deckCard, instance);
        }

        private void OnRemove(DeckCard deckCard)
        {
            var instance = _instances[deckCard];
            Destroy(instance.gameObject);
            _instances.Remove(deckCard);
        }
    }
}
```

Core内の変更を検知して、UI側のオブジェクトを生成、破棄。

### Infrastructure
```C#
using System.Collections.Generic;
using System.Linq;
using Databases;
using DeckEdit.Domain.UseCases;
using VContainer;

namespace DeckEdit.Infrastructure
{
    public class DatabaseCardListLoader : ICardListLoader
    {
        [Inject] private readonly CardDatabase _database;
        
        public IEnumerable<string> LoadCardList()
        {
            return _database.CardDatas.Select(value => value.cardKey);
        }
    }
}
```

カードプールのロードの実装。
今回は、データベースにあるデータをそのまま使用しているだけ。

### 他
データベースの本体。ScriptableObjectを継承。
```C#
using System;
using System.Collections.Generic;
using UnityEngine;

namespace Databases
{
    [CreateAssetMenu(menuName = "Create CardDatabase", fileName = "CardDatabase", order = 0)]
    public class CardDatabase : ScriptableObject
    {
        [Serializable]
        public class CardData
        {
            public string cardKey;
        }

        [SerializeField] private List<CardData> cardDatas;
        public IReadOnlyList<CardData> CardDatas => cardDatas;
    }
}
```

# 感想
これはクリーンなアーキテクチャであってクリーンアーキテクチャ（固有名詞）ではない！！！
前回はクリーンアーキテクチャの概念（というか例の図）に振り回されていたが、設計はもっと自由でいいんだ。
少なくともSOLID原則さえ守れていたら、最悪依存の一方通行さえ守れていたら、最悪の最悪相互依存になっていなければ自由に書いていいと思った。