# 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つがちょうどよさそう
-