# Lite Architecture 現在このリポジトリでは、「まずは実装」を第一に、クリーンアーキテクチャを部分的に取り入れ、環境の切り替えにおける柔軟性には配慮しつつ、UIの実装部分でのみ制約を排除した設計思想を維持しています。 この文書では、この環境がどのような構成なのか、この環境においてどのようなロジックフローが組まれているのか、この環境で実装を行うためにはどこを触ればいいのかなどを説明します。 #### 目次 [TOC] ## アーキテクチャのUML表現 後でchatgptに書いてもらう ### 良く取り扱われるクリーンアーキテクチャ+MVPの実装 ```plantuml package Domain { class Object enum ObjectType Object *- ObjectType : «composition» } package Application { interface IObjectRepository class FeatureService interface IFeatureService IObjectRepository .u.> Object : «use» FeatureService -|> IFeatureService : «implements» } package Infrastructure { static class ObjectDao { + ObjectModel FromDomain(Object); + Object ToDomain(ObjectModel); } class ObjectRepository class Database class ObjectModel ObjectRepository -u-|> IObjectRepository : «implements» ObjectDao <. ObjectRepository : «use» ObjectModel <.u. ObjectDao : «converts» ObjectDao .u..> Object : «converts» Database <.u. ObjectRepository : «use» ObjectModel -* Database : «contain» } IObjectRepository <.l FeatureService : «use» class DI DI .u.> FeatureService : «get» package Presenter { class FeaturePresenter FeaturePresenter .u.> IFeatureService : «interacts» DI .> FeaturePresenter : «injects» } package View { interface IFeatureScene class FeatureScene IFeatureScene <.u. FeaturePresenter : «interacts» FeatureScene -|> IFeatureScene : «implements» FeatureScene <.u. DI : «get» } package UnityEditor { class TestRunner class MonoBehaviour TestRunner .u.> ObjectRepository : «tests» MonoBehaviour <|-u- FeatureScene : «extends» } ``` クリーンアーキテクチャというあからさまな名前から非常にもてはやされ、とりあえず入れとけ感のある設計。MVPも同様に、MonoBehaviourとPureC#の間の依存排除というと真っ先に名前が挙がるほど有名なやつ。だがこれらは、プロジェクトに導入するにあたっていくつかの障壁がある 1. わかりづらい これに限る。わかりづらい。C#を初めて触る人間がやる構造ではない。 2. 冗長性とファイル数の増大 邪魔。DIや責務制限のためのinterfaceが増えすぎて、完全に理解した人間でないとどれがどれだかわからなくなる。結局実装がどこか分からなくて迷子になる例多数。 3. Presenterの構築で学習コストと工数が増加する ライブラリなしで純粋にMVPをやろうとすると、1つのUIが1つのロジックを呼ぼうとするたびにPresenterで2プロパティ4メソッド2メンバ変数2コンストラクタ引数の追加が発生し、結局UnityEditor上での操作や再構成も発生する。 これを解決する方法としてZenjectやVContainerといった有名どこのDIライブラリがあるが、学習コストが高すぎるし概念が難しい。 ただ、アプリ実行とテスト環境の切り替えは通らなければいけないので、この中からPresenterとDIを避けてみると現行の形になる。 ### 現在の状況の概略図 ```plantuml package Domain { class Object enum ObjectType Object *- ObjectType } package AppCore { interface IContext interface IObjectRepository interface IService class FeatureService IObjectRepository .u.> Object IObjectRepository -* IContext IContext *- IService IService <|- FeatureService } package Infrastructure { class LiteDBManager class DObject static class ObjectDao { DObject FromDomain(Object); Object ToDomain(DObject); } class ObjectRepository class Context ObjectDao <. ObjectRepository LiteDBManager -u-* ObjectRepository DObject <. LiteDBManager LiteDBManager -u-* Context DObject <.u. ObjectDao ObjectDao .u.> Object ObjectRepository -u-|> IObjectRepository Context -u-|> IContext } package Presentation/View { class InAppContext class FeatureScene InAppContext *-u- IContext FeatureScene -u-> FeatureService } package Runner { class AppRunner AppRunner .u.> Context AppRunner .u.> InAppContext } package Test { class MockObjectData class TestContext class TestFeatureService MockObjectData -* TestContext TestContext .u..> Context TestContext <- TestFeatureService } package UnityEngine { class MonoBehaviour class TestRunner MonoBehaviour <|-u--- FeatureScene MonoBehaviour <|-u-- AppRunner TestRunner .u.> TestFeatureService } package Native{ interface INative class NativeService NativeService -- INative FeatureService .u.> NativeService } ``` 本来は分離してModelとViewが注入すべきPresenterを省略し、ViewがUseCaseであるServiceに直接依存している。 これにより良く言えば冗長なコードを削減して機能の実装を優先できる状態にしてある。 悪く言えば密結合、Serviceの変更がSceneとTestの両方に影響を及ぼすため変更が大きくなりやすい。が、こんなものは序盤なら当たり前なのであって、そういう維持管理コストの低下や拡張性の話はある程度中身ができてから考えたほうがいい。 この環境で主要な機能が実装できてきたら、Presenterを導入してMVPに切り替える等の変更を行えば大丈夫。 ### レイヤーのみによる簡略図 ```plantuml object Domain { + .Enum + .Entity } note left: 全体で使う構造体の定義 object AppCore { + .UseCases + .Interfaces + .Utilities } note left: UseCases: 全体で使う機能群の定義\nInterfaces: 下位層で使う機能の宣言\nUtilities: 補助機能の実装 object Infrastructure { + .Data.Schema + .Data.Dao + .Repository - Context.cs } note left: Schema: DBtableの定義\nDao: DBClass↔DomainClass\nRepository: CRUDの実装 object Presentation { + .Presenter + .View.** - InAppContext.cs } note right: Presenter: AppCoreとViewの接続\nView: UIの入り組んだ実装 object Test { + .MockData + .Unit + .Integration - InTestContext.cs } note left: MockData: テストに使うデータ群定義\nUnit: Repositoryのテスト\nIntegration: UseCaseのテスト object Runner { - AppRunner.cs } note right: アプリ環境を定義してContextを注入 object NUnitFramework { + TestRunner } object Unity { + UnityEngine } Domain <-d- AppCore AppCore <-d- Presentation Infrastructure -u-> AppCore Infrastructure <-d- Test Presentation <-d- Runner Test <-d- NUnitFramework Runner <-d- Unity ``` ## ロジックフロー 現在の状況の概略図を用いて実行の大まかな流れを解説する。 以降の文章において、`Object`はアプリ内で利用する構造体(ex: Schedule構造体)を指し、`Feature`は構造体を利用したアクション群(ex: Scheduleの操作)を指す ### アプリ実行時のフロー 1. AppRunnerが起動し、Contextを生成する 2. ContextがLiteDBManagerを生成し、各種ObjectRepository, IServiceを生成する 3. AppRunnerがInAppContextにContextのインスタンスをIContextとして渡す 4. FeatureSceneに対するユーザーのインタラクションが発生する 5. FeatureSceneがFeatureServiceの内容を呼ぶ 6. FeatureServiceがInAppContextからIContextを取得する 7. FeatureServiceがIContextからIObjectRepositoryの実装を呼ぶ 8. ObjectRepositoryがLiteDBManagerからDObjectをもらってくる 9. ObjectRepositoryがDObjectをObjectDaoでObjectに変換する 10. ObjectRepositoryがObjectをFeatureServiceに返す 11. FeatureServiceが受け取ったObjectを加工してFeatureSceneに返す 12. FeatureSceneが演出する ### テスト実行時のフロー 1. TestRunnerがTestFeatureServiceのTestメソッドを呼ぶ 2. Testメソッドの先頭でInTestContextのContextを取得する 3. InTestContextのgetterがTestContextを生成する 4. TestContextがLiteDBManagerを生成し、ObjectRepositoryとIServiceの生成準備をする 5. 必要に応じてMockObjectDataからデータ群を取得し、LiteDBManagerに登録する 6. TestContextからFeatureServiceを取得してテスト対象のメソッドを実行する 7. FeatureServiceがTestContextのIContextからIObjectRepositoryの実装を呼ぶ 8. ObjectRepositoryがLiteDBManagerから今さっき追加されたMockのDObjectをもらってくる 9. ObjectRepositoryがDObjectをObjectDaoでObjectに変換する 10. ObjectRepositoryがObjectをFeatureServiceに返す 11. FeatureServiceが受け取ったObjectを加工してTestFeatureServiceに返す 12. TestFeatureServiceがテストの成功を判定する 13. 最後にTestContextをDisposeするのを忘れずに ## 実装手順 実装は要素ごとに分けられるしその役目によって実装場所ももうほぼ決まっている ### namespace別 Assembly Definitionによる制約 基本的には開発に必要なルートnamespaceはすべて宣言してあるからScript直下にはファイル/ディレクトリを作成しない それ以外でも、多分Presentation.View以下以外ではディレクトリを作る必要がない それぞれのnamespaceに対してはビルド時間の短縮、また責務と設計の遵守のため、特定の名前空間の情報しか参照できないようにしてある - namespace Domain - 何も参照しない PureC#を書け - 全体で利用する構造体の定義 - namespace AppCore - Domainのみ参照可 - Interfaceの定義とServiceの実装 - namespace Infrastructure - Domain, AppCoreのみ参照可 - データベースモデルとRepositoryの実装 - namespace Presentation - Domain, AppCore, UnityEngine参照可 - シーン上に置くスクリプトは全部ここのViewに - InAppContextはここからしか使えないのでContextの濫用はできない - namespace Test - Domain, AppCore, Infrastructure, UnityEngineのみ参照可 - Serviceの統合テストを作る - UnityEngineはTestRunnerのために置いてるのであって間違ってもMonoBehaviourを継承するな - InTestContextは此処からしか使えないのでContextの濫用はできない - namespace Runner - Bootstrap専用なのでここにクラスを作らない ### 実装の要素分解と実装箇所 - 公共構造体 => namespace Domain - 全体で使う構造 (ex: Schedule, ScheduleType, Time) - Domain.Entity と Domain.Enum があるので区別 - データベースのレコード追加 => namespace Infrastructure.Data - Domainに追加した構造体は基本レコードとして保存することにもなる - Infrastructure.Data.Schemaに、すべてのメンバ変数がpublicなgetとsetを持つプロパティとして型を宣言する - LiteDBによる制約、強制力はないがそうでないメンバ変数は保存されない - Infrastructure.Data.Daoに、Schemaに追加したレコードとDomainに追加した構造体を相互変換するpublic staticなメソッドを実装する - 追加したレコードのCRUD => namespace AppCore.Interface, Infrastructure, Test - AppCore.InterfaceにRepositoryのinterfaceを実装する - AppCore.Interface.ContextにRepositoryのinterfaceを取得するプロパティを宣言する - Infrastructure.Repositoriesにはそのinterfaceを実装するクラスを実装する - コンストラクタにはLiteDatabaseか対象のILiteCollectionを渡すといい - Infrastructure.Contextにその実装したクラスを作成して渡すプロパティを実装する - Test.InTestContext.TestContextに実装したクラスを作成して渡すプロパティを実装する - Test.UnitにRepositoryのメソッドすべてを確かめるテストコードを実装する - 新機軸の機能を追加する => namespace AppCore.Services, Infrastructure, Test - AppCore.IServicesを実装するServiceクラスを実装する - Infrastructure.Contextの初期化で実装したServiceクラスを持たせるように実装する - Test.InTestContext.TestContextにServiceクラスを作成する表現式を持たせる - Test.IntegrationにServiceのメソッドすべてを確かめるテストコードを実装する - 既存軸の機能に機能を追加する => namespace AppCore.Services, Test - 既存のServiceクラスに機能を追加する - Test.Integrationに追加したメソッドを確かめるテストコードを実装する - Unity側のViewにつけるコンポーネントを作成する => namespace Presentation.View - いい感じのView以下namespaceを見繕うか、新しいnamespaceを作る - Scene毎に1つがちょうどよさそう -