yotiky Tech Blog

とあるエンジニアの備忘録

Unityを取り巻くアーキテクチャ 第1部 「MVxとLayered Circle」

最近Unityをゆるく触ってる感じで、"やりたいことをやれるやり方で"実装すると「こりゃ死ぬな..」と思ったので、界隈の設計方針調べてるところです。

アーキテクチャは読み手の解釈に委ねられる部分が多かったり、提唱されたあとも進化や変化があったりするので、あくまで現時点でそれぞれどう捉えてるかの覚書と考察を。雑ならくがき、雑がき。

※編集したり更新したりする予定です。

目次

概要

つらつらと書き始めたら思いの外風呂敷が広がってしまったので、「MVxとLayerd Circleについて」、「DDDについて」、「Unityで実装する話」の3部構成くらいに収まるといいなぁと言った所です。
今回の記事は第一部「MVxとLayerd Circleについて」にあたります。

一応自分のアーキテクチャ絡みそうな経験を軽く記す。 大体C#使って開発してて歴はそこそこ長め、古くはクラサバや3層アーキテクチャWPFでMVVMのまさかり飛んでる頃にはWPFのプロジェクト携わったり、ゲームのサーバーサイドをASP.NET MVCでやったりなどなどしてました。

アーキテクチャ

アーキテクチャは、アプリケーションをどう動かすか、どう設計するかのパターン。
アーキテクチャは、性質上読み手の解釈が異なることも多く、定義自体の”解釈の議論”が発生してとても不毛だとは思う。
より明確であったなら、使えるか使えないか、どう使うかの判断だけで済むのだが、プロジェクトの状況によってコンテキストも違うので全部ケースバイなんて無理だし難しいよねと。テクノロジーや時間経過によっても変化や進化はあるし。

ただアーキテクチャは経験則によるパターンなので、「何に対してどう対処したのか」を自分なりに理解し、使える武器として揃えておくことは有用だ思う。使える時に使えばいいし、合わなければ使わなければよい。

名前をつけて提唱された"アーキテクチャ"は、アプリケーションに骨子を与える。登場する箱は主なキーパーソンでしかない。それ以外に定義しちゃダメってわけでもないし、箱すら分離して崩したっていい。そもそも着眼点以外がふわっと定義されてたりもするし、プロジェクトに合わせて柔軟に対応すればいい。
最終的には我々がやってることは学問じゃないので、アーキテクチャが正解かどうかではなく、目の前の問題に対処できたかどうかの方が大事だ。
ひとつ注意するとすれば、アーキテクチャの目的、意味は理解しておきたい。アーキテクチャの目的を理解していないと、名のある"アーキテクチャ"が"アーキテクチャ"として意味をなさなくなるし、箱の名前と役割も変わってくる。

「MVx」と「Layered Circle」

「MVx」は、Viewとそれ以外をどうつなぎ合わせるかのアーキテクチャ
"それ以外"ってのが肝で、"x"にあたる"つなぎ方"以外は全部Modelとし表現してる。この解釈を間違えると"x"にビジネスロジックが入り込んでまさかりが飛んでくる。

「Layered Circle(Hexagonal/Onion/Clean Architecture)」は、ドメイン(ビジネスルール)とそれ以外をどう分離するかのアーキテクチャ
Hexagonal / Onion / CA は細部(表現)は違えど目的は同じ。
如何にドメイン(ビジネスルール)を外的要因から隔離し、ドメイン(ビジネスルール)をCleanに保つか、これが肝。

これらは視点が異なる。 だから、同列に並べてどちらかを取捨選択するものでもない。組み合わせられるものは組み合わせればいいし、コンセプトの違いで組み合わせられないものはそもそも選べない。

MVx

MVC」、「MVP」、「MVVM」などが存在するが、詳細は各々調べて自分なりの解釈を探ってもらうとして、雑に筆者の解釈を記すと、

「C」は入力を受けて「M」に処理を依頼、「V」を選択して出力する。「C」が入力を受けるためにフレームワークを使ってルーティングすることが多い。「V」から直接入力されるわけではない。

f:id:yotiky:20180926043432p:plain

「P」は「V」のイベントをハンドリングして「M」に処理を依頼、その結果を「V」に反映する仲介役。「P」が主体となり「V」と「M」を調整する。(PassiveとかPushとかいう方)

f:id:yotiky:20180926043441p:plain

VM」は「V」に最適化されたデータオブジェクト。「V」と表裏一体にくっついてて、「P」のようにイベントをハンドリング、または「V」から叩かれたりして「M」を呼ぶ。「V」は「VM」と、「VM」は「M」とバインディングすることで、「M」の値の変更通知を「VM」が受け自身に反映する。また「VM」の変更通知は「V」へ伝播し「V」が更新される。

f:id:yotiky:20180926043448p:plain


凡例
f:id:yotiky:20180928024130p:plain

  1. 関連。矢印の先端を参照することを意味する。矢印の先端に依存する。
  2. 関連に対する戻り値。矢印の先端に対して値が戻される。
  3. 事前にバインディングすることで、イベント通知または値の変更通知が矢印の先端に対して発行される。双方向の場合、値の変更通知が双方向の場合と、イベント通知と値の変更通知が混合されてる場合も含む。
  4. 関連に対する「実質の呼び出し」として使用した。

MVVMの関連について

MVVMを極めし人々は、View→ViweModel、ViewModel→Modelにおいてはvoidのメソッドを叩き、変更通知が伝播することでViewに結果が投影されるという。 1
WikipediaによるとMVVMの提唱者はMSのJohn Gossmanらしく、2005年のBlog 2ではデザイナーとの作業の分離に着目して書かれてて、要はViewとそれ以外を分離してどう紐付けるかの話をしている。分業に夢を抱いて大きく舵を切ってた時代。
一方で国内WPF界隈で発展したMVVMに関する知見は、個人的には哲学的に昇華したという捉え方で、実務においてはそこに囚われすぎ無くても良いと思っている。具体的には、ViewModel→Modelにおいては、ケースバイケースで戻り値を持つメソッドを叩いてもいいのでは、と考える。

f:id:yotiky:20181031024402p:plain

ViewModel→Modelにおいてvoidのメソッドを叩くのが引っかかるのは、変更通知の伝播が最低2層、Model内で伝播が発生すると3層以上になってしまう点。
手に乗るサイズのコード量であれば気にしすぎなケースもあれど、多人数での作業だったりコードに歴史が重ねられると何が何に関連しているか把握しづらくなってしまう。
ViewModel層で介在(フィルタ)したり、伝播の粒度(束ねたり)なんかも気にしたくなるケースがあったりなかったり。水の波紋のように中心に触れると綺麗に広がってくのは気持ちが良いとは思うけど。

Layered Circle

参考にさせていただいたサイトによると、Hexagonal Architectureが2005年(PoEAA的には2002年?)、Onion Architectureが2008年、Clean Architectureが2013年に提唱されたようだ。

Hexagonal Architecture

PoEAAでパターンとして提唱されたものらしい。
目的は、自動化された回帰テスト
本質は、「内部(アプリケーション)」を「外部」から切り離すこと。
主なキーワードは、アプリケーション、ポート、アダプター、外部デバイス
テストを外部デバイスGUIやDB)に依存せず、ドライバー/モックを使ってアプリケーション(ビジネスロジック)だけを繰り返しテストさせるためのアーキテクチャ、というかパターン。
サンプルはあるもののそういう風にしようぜってだけで、具体的にアーキテクチャとしてどう組もうまではなさそう、回帰テスト目的なのでそこまでってことか。 ポートは差込口で、アダプターはそのポートに差し替え可能な外部デバイスとの繋ぎ部分。例えばDBアクセスのロジックとそれに差し替わるMock(これは外部デバイスないので自身で完結)は、一つのポートにぶら下がるアダプターとなる。
ポートは6個あるわけではなく、複数あるよってことでHexagonの形。

f:id:yotiky:20180926043543p:plain

他のアーキテクチャの影響を受けて、解釈は大きく、進化してそうな気配はある。


凡例
f:id:yotiky:20180928024318p:plain

  1. インターフェイス
  2. 実現。インターフェイスを実装することを意味する。

Onion Architecture

提唱者はMicrosoft MVPっぽい。Microsoft のテクノロジがベースとなったアーキテクチャのようだ。
目的は、あまり安定しない(変化しやすい)コードから影響を受けないようにすること。
主なキーワードは、外周のUser Interface, Infrastructure, Testsと、
Application Core と呼ばれる内側3層がApplication Services, Object Services, Object Model。

Infrastructureを外部化しアダプターコードを間に置くことで疎結合にする事は、Hexagonal Architectureと同じとなる。ただ、ドメインをCleanに保つこと自体を目的とはしておらず、副次的にそうなると言った感じか。事実DDDの有無にはかかわらず機能するとなっている。

原典によると、小規模なWebサイトには適していないし、複雑なアプリケーションや寿命の長いビジネスアプリケーションに適しているとなっている。これはBehavior Contracts(動作の取り決め)のためにInterfaceの使用を強要し、依存関係逆転によりInfrastructureの外部化を強制するため。

OnionにおけるInfrastructureはData層に当たると思われるが、レイヤードアーキテクチャの図ではUIからも参照されてて謎めいているが、Utility Servicesを含みそうな書き方なのでその矢印だろうか、、Part3においてUIからInfrastructureへの矢印は消えているのだが、、、罪深い。

f:id:yotiky:20180926043550p:plain

Onion Architectureの特徴の一つは、他のアーキテクチャではレイヤーは隣接するレイヤーに結合するのに対し、Onion Architectureは内側であればレイヤーを跨いで結合することができる点にある。
変化しやすいコード、User InterfaceおよびInfrastructureの実装部分は外周におき、内部のビジネスルールはInterfaceを使った関連を築き、処理を実装する。
必須ではないものの、IoC(Inversion of Control:制御の反転)コンテナ、DIコンテナ等を使うことを強くお勧めしている。

f:id:yotiky:20180926043557p:plain

Clean Architecture

Hexagonal、Onion、その他いくつかのアーキテクチャを受けて焼き直したアーキテクチャ
目的は、フレームワーク、UI、DB、外部機能からビジネスルールを独立し、さらにそれだけでテスト可能とすること。
表現が違うだけで本質は同じ。ビジネスルールを外的要因から分離する。
主なキーワードは、
Frameworks&Drivers : UI, Web, DB, Devices, External Interfaces
Interface Adapters : Controllers, Presenters, Gateways
Application Business Rules : Use Cases
Enterprise Business Rules : Entities

ビジネスルールはフレームワーク、UI、DBから独立しており、そのためUIはウェブからコンソールへビジネスルールの変更なしに置き換えられる。 一番外側は外的なモジュールやツール、DB、UIだとUIで使われるフレームワークやライブラリ、その他諸々の業務の関心ごと以外となる外的要因。

f:id:yotiky:20180926043608p:plain

個人的解釈のポイント。

EntityはEnterprise Business Rules層に属しており、ビジネスルールをカプセル化するので所謂POCOではない。ドメイン、ビジネスルールと呼ばれる部分。システム化することに依存しないビジネスルール。

Use CaseはApplication Business Rules層で、上記ビジネスルールをシステム化することで必要となる繋ぎ、Facade的な役割を持つ。EntityはUse Caseの影響を受けないとなるので、真ん中にもやはりInterfaceが必要なのかもしれない。

Interface Adapters層ではアプリケーション内部のビジネスルールに適したデータ形式と、外部で利用するのに適したデータ形式とを変換する。また外側のFrameworks&Drivers層とApplication Business Rules層の処理の橋渡し役となる。

原典によると、「MVC」は、Mは単なるデータ構造とし、VCはInterface Adapter層ってことで説明されてる。Controllerは入力を受けて処理を依頼し、UseCaseでEntityを経由(ビジネスルール)したのち、Presenterが出力を仲介する。(なぜかPが登場...)

Frameworks&Drivers層は、DBやWebフレームワークとなっている。DBそのものはアプリケーションの境界外であるはずなので外に追い出して、そことの繋ぎ(Driverやそのラッパーあたり)を持ってきた。それは詳細を知っていて詳細をすべて内包する形で影響を閉じ込める。

境界をまたがるいたるところで、依存関係逆転の原則、すなわちInterfaceと実装による疎結合が使われるとあるのでこのような実装になると思われる。
境界を流れるデータについても、外側の円において、内側の円にとって使いやすい形にすることになるだろう。(データを使い回さず各層で詰め替えるならば)Interfaceが内側の円にあるので自然とそうなるはず。

参考にさせて頂いたサイト