Unityで
オニオンアーキテクチャ
2019/02/21 Roppongi.unity
とりすーぷ
自己紹介
• とりすーぷ
• バーチャルキャスト社
• Unityエンジニア
オニオンアーキテクチャとは
• 「こうやってプログラムを作ろう」という設計指針
• 考え方の根幹はドメイン駆動設計(DDD)
ドメイン駆動設計とは
ドメインって何
• ドメイン(知識、専門領域)
• そのプロダクトが本質的にやりたいことそのもの
• そのプロダクトが扱う必要がある知識
• 処理に必ず必要なデータ
ドメイン駆動設計(DDD)とは
• ドメインを中心にモノゴトを考えるやり方
• 「このプロダクトが本質的にやりたいところ」
• 「実装都合で仕方なくそうなってしまっているところ」
• この2つを明確に区別してシステムを作っていこうねってやり方
たとえば
• セーブデータの保存
• 本質的にやりたいこと「データの保存と復元」
• データが保存できて復元できれば実現方法はなんでもいい
たとえば
• セーブデータの保存
• 本質的にやりたいこと「データの保存と復元」
• データが保存できて復元できれば実現方法はなんでもいい
• 実装都合で変わるところにはどうでもいい
• ローカルに書き出し?サーバに保存?
• Json? Xml? Byte[]?
• 平文? 暗号化?
たとえば
• セーブデータの保存
• 本質的にやりたいこと「データの保存と復元」
• データが保存できて復元できれば実現方法はなんでもいい(これがドメイン)
• 実装都合で変わるところにはどうでもいい
• ローカルに書き出し?サーバに保存?
• Json? Xml? Byte[]?
• 平文? 暗号化?
オニオンアーキテクチャ
オニオンアーキテクチャ
• 大きく4層で構成されるアーキテクチャ(と今回は定義)
• Domain
• Application
• Infrastructure
• Presentation
この4層にスクリプトを書く
Domain層
• 本質的にやりたい処理
• 動作に必要な重要なロジック
• 処理に必要なデータ構造
実際の開発ではインタフェースや
構造体が定義されることが多いレイヤ
Infrastructure層
• 実装を置くところ
• 機能追加や修正が発生すると
ここに対して変更を加えることになる
Application層
• 処理の手続きを書くところ
• Domain層のオブジェクトに対しての
操作をまとめる場所
• 比較的に薄くなることが多い
Presentation層
• UnityとApplication層をつなげる層
• UIからの操作をApp層に渡す
• App層から受け取った結果をUIに反映する
• MonoBehaviourと連携する
Unityで扱う上でのルール
• MonoBehaviourを継承できるのは原則Presentation層のみ
• ただし、継承しないとどうしようもない場合はInfra層で継承してもよい
• Domain層でもUnityEngine.Vector3などのデータ構造は触れて良い
• ただしDomain層からComponentやMonoBehaviourを触るのは禁止
• IObservable<T>やUniTask<T>などはドメイン層でも使う
• 使わないと逆にややこしくなるため
DIを当たり前にように使う
• DI Containerが無いとこのアーキテクチャで開発は厳しい
• Zenjectは必須
クリーンアーキテクチャとの違い
• 本質はまったく同じことを言っている
• 違う点はレイヤ区分の細かさ
• オニオン:粗め(大きく4つにしか分けてない)
• クリーン:かなり細かい
• 触ってみてとっつきやすい方から触ればいいと思う
実装例
データをセーブ&ロード
• 入力パラメータを保存
• ロードで復元
• データの保存先:ローカル
• データの形式:PlayerPrefs
実装の手順
1. Domain層を考えて定義
2. Inra層に実装
3. App層に手続きを書く
4. Presentation層から呼び出す
0. 下準備 ディレクトリを別ける
• それぞれのレイヤごとにディレクトリを用意
• ついでにAssembly Definition Fileも定義すると
レイヤの依存関係を制御できるのでおすすめ
←Infra層はDomain層しか参照
できない形に強制できる
1. Domainの定義
• Domain層をまず第一に考えて設計する
• 「名前」が大事になるのでしっかり考えて
• ここで決定した命名は途中でブレることなく一貫した意味を持つ
• EntityとValue Objectをはっきり区別しよう
• 詳細はググって
Domain.SaveData型
• セーブデータを扱う型
• struct / class はよく考えて
Domain. ISaveDataRepository
• SaveData型の読み書きをするもの
• インタフェースになっているのが重要
2. Infra層に実装を書く
• PlayerPrefsSaveDataRepsitory
• ISaveDataRepositoryを実装
• 内部でPlayerPrefsを使う
3. Application層に手続きを書く
• Domainのオブジェクトを呼び出す部分
• 処理をそのまま素通しするパターンもある
4. Presentation層でUnityと繋ぐ
• Model
• データ管理
コンポーネント
4. Presentation層でUnityと繋ぐ
• Presenter
• UIの更新コンポーネント
5. ZenjectのInstallerを書く
• ここで「何の実装を使うか」が決まる
省略記法もある:BindInterfacesAndSelfTo<PlayerPrefsSaveDataRepository>()
6. できた!
仕様変更1
• Jsonで保存してほしい!
1. Jsonで保存するRepositoryを追加
• Domain.ISaveDataRepositoryを実装したものを新しく追加
2. Installerを書き換える
3. おわり!
• 実装を追加して、Installerを書き換えたら仕様変更できた!
仕様変更2
• SaveDataがなかったときはデフォルト値を返して欲しい
1. デフォルト値を定義
• 言葉の意味はよく議論すること
• 「デフォルト値って何?」
• アプリケーション上で一意に定まる固定値…?
• UnityEditor上で設定できる可変値…?
• それが何を意味する単語なのかはしっかり決めること
1. デフォルト値を定義
• 今回は「Domain上に定義されている固定値」にした
2. Application層を修正
• Nullが帰ってきたらDefault値で差し替える
まとめ
メリット
• 機能追加・修正にめちゃくちゃ強い
• とくにアウトゲーム部分と相性がよい
• どこに何を書けば良いかが明確になる
• テストが書きやすくなる
デメリット
• インゲームには適用しにくい
• 処理内容に対して記述量がかなり増える
• Domainの設計がかなり難しい
• インタフェースの使い方をわかってないと無理
• Zenjectが難しい
まとめ
• まだ試行錯誤中
• 個人開発しているアプリで試験運用中
• 業務アプリで本格採用はまだしてない
• そもそもDDDが難しい
• 何もわからん…何も…
今回のプロジェクト
• https://github.com/TORISOUP/OnionSample

Unityでオニオンアーキテクチャ