nuits.jp blog

C#, Xamarin, WPFを中心に書いています。Microsoft MVP for Development Technologies。

【Xamarin】Blue Monkeyプロジェクト Architecture Overview コメント入り掲載

【追記】
本エントリーは大幅に見直す予定です。現在検討中の内容は以下に公開しています。
Blue Monkey Architecture Overviewに対する訂正 - nuits.jp blog
【追記終了】

先日Infragisticsさんのイベント「Infragistics Day 2017 Spring」にて、現在進行形で進めているOSSプロジェクトBlue Monkeyの件でお話しさせていただいてきました。

jp.infragistics.com

またその時のスライドも掲載させていただいていますが、何分スライドだけ見ても全く分からない為、しゃべった内容を大まかにまとめて記載しておこうと思います。 見ただけで分かって、なおかつ聞いているときも見やすい資料って難しいですね。。。

という訳でよかったら見ていってください。

Blue Monkeyプロジェクト Architecture Overview

f:id:nuitsjp:20170315215713j:plain

BlueMonkeyプロジェクトのゴール

f:id:nuitsjp:20170315215716j:plain

BlueMonkeyプロジェクトのゴールは

一定規模のチーム開発に耐えられるアプリケーションアーキテクチャのリファレンス実装を提案することにある

と考えています

特に重視しているのは、テスタビリティやメンテナンスビリティです。
そしてそれらを維持したまま、どうやってクロスプラットフォーム環境で実現するのか?というのが重要なポイントです。

ちなみにバックエンドは、現時点ではAzureで作っているんですが、基本的にバックエンドにはがっつり依存せず、差し替えが可能な形で作成しています。 将来的にはAWSやGoogleのMBaaSやRealmなどにも対応したいと考えています。

もう一つ、難しすぎて理解が困難にならないというのも、個人的には考慮しています。 DDDやClean Architectureを完璧に理解されていて、モバイル開発に慣れている方には不要なものだと思います。

アーキテクチャ上のキーワード

f:id:nuitsjp:20170315215717j:plain

なお、Blue Monkeyのアーキテクチャ関連の話題だけでも、しゃべり(書き)始めたら切りがありませんので、時間の都合上今回の発表では上記の3点に絞って話をさせていただきました。

Prism for Xamarin.Forms

f:id:nuitsjp:20170315215718j:plain

BlueMonkeyはXamarin.Formsを利用したアプリケーションです。
このため、MVVMパターンを採用しています。

MVVMパターンを採用した場合、Xamarin.Formsに限らずWPFやUWPでも同様ですが、それらだけでアプリケーションを構築するのは結構辛いです。

MVVMパターンで辛くなりがちな点

f:id:nuitsjp:20170315215719j:plain

特につらいのは、ViewModelからViewを操作するケースです。
画面遷移や、確認ダイアログ・選択ダイアログなどです。
このため、何らかのMVVMサポートフレームワークを選択するのが一般的です。

BlueMonkeyではPrism for Xamarin.Formsを採用しています。
ここでは、あまり詳しくないという方への説明も兼ねて、Prismについて簡単に説明してみたいと思います。
詳細は本ブログでも取り上げているので、良かったらこちらからどうぞ

What is Prism?

f:id:nuitsjp:20170315215720j:plain

Prismの製作者であるBrian Lagnus氏は次のようにおっしゃっています。 プリズムはXAMLアプリケーションフレームワークです。 そして、プリズムはガイダンスであり、パターンやプラクティスの集合でもあります。 プリズムを使うと、アプリケーションは自然とテストしやすく、変更も容易になります。

What do you get?

f:id:nuitsjp:20170315215722j:plain

プリズムは上記のものを提供しますが、その中でも赤字の部分。

  • Navigation
  • Page Dialog Service
  • Dependency Injection

この辺りが非常に強力です。

ただ私は、こういった個別の機能よりも大切なものがPrismには含まれていると考えています。

Prismの真の強み

f:id:nuitsjp:20170315215723j:plain

それはPrismが

  • ガイダンスであり
  • パターンやプラクティスの集合

であるということです。
そしてそれらが、みなさんのアプリにテスタビリティやメンテナンスビリティをあたえてくれる

これこそがPrismの真価だと思っています。

MVVMパターンとは

f:id:nuitsjp:20170315215724j:plain

BlueMonkeyはXamarin.Formsを利用したアプリケーションです。 このため、MVVMパターンを採用しています。

よくみかけるMVVMの図

f:id:nuitsjp:20170315215725j:plain

XAMLアーキテクチャのアプリの設計というとMVVMをはじめMVC・MVPVMなどがフォーカスされがちです。
これらは全てプレゼンテーション層に関する設計ですが、フォーカスされるには理由があって、プレゼンテーション層は

  • 専門性が高かったり
  • テストが困難だったりすることが

多いためで、それらを解決することが強く求められているからです。また一般論として議論しやすいからという側面もあるのではないでしょうか。

MVVM is 何

f:id:nuitsjp:20170315215726j:plain

そもそもMVVMはXAMLアプリケーションを構築する神器みたいに言われることが多いですが、その本質はいったい何なのでしょうか?

MVVM is PDS

f:id:nuitsjp:20170315215727j:plain

MVVMというのは、Presentaion Domain Separation(PDS)と呼ばれる 「プレゼンテーション層」と「それ以外」を分離するための仕組みの実現手段の一つです。
PDSにはMVVM以外にも、MVCやMVPVMなど多数の「手段」が存在します。

PDS is SoC

f:id:nuitsjp:20170315215728j:plain

そして実のところ、PDS自体もそれを包括するSeparaiton of Conserns(SoC:関心の分離)の一つの手段でしかありません。
アプリケーションは一つの大きな塊で構築するより、小さく分けて開発して、それらを統合した方が良い。
この考え方は多くの方と共有できるのではないでしょうか?
そして大雑把にいうと、SoCとは、その考え方そのものです。

つまり

  • アプリケーションを分割して構築しようというSoCの考え方の内
  • プレゼンテーション層のみに焦点を当てたのがPDSです
  • MVVMとはPDSを実現する手段の一つにすぎません

MVVMはアプリケーションのほんの一部分を解決する手段でしかないことが理解いただけるかと思います。

SoC Overview

f:id:nuitsjp:20170315215729j:plain

ちなみにプレゼンテーション層以外でSoCを実現する手段として有名な概念に

  • Inversion of Control(IoC)

というものが存在します。
そしてIoCの実現手段のとして、特に有名なのが

  • Dependency Injection(DI)
  • Service Locator

の2種類があります。

つまり、MVVMにおけるModelというのは…

f:id:nuitsjp:20170315215731j:plain

プレゼンテーション以外全て、という意味を表します。

実際の割合

f:id:nuitsjp:20170315215736j:plain  

しかし、実際のアプリケーションの比率をみるとModelはプレゼンテーション以外の全てですから当然VとVMを全て合わせたものよりも、Modelが圧倒的に分厚くなります。
つまり、アプリケーションをMVVMで決めましょうということは、アプリケーションのごく一部のアーキテクチャを決定しただけに過ぎないということです。

Mobile & Cross Platform開発

f:id:nuitsjp:20170315215733j:plain

さて、特にXamarinのようなモバイルでかつクロスプラットフォームの場合、実はモデルの中にも

  • 専門性の高い領域や
  • テストが難しい領域

が多数存在します。

Mobile & Cross Platformは課題の山

f:id:nuitsjp:20170315215734j:plain

例えば プラットフォーム依存領域や、時間、非同期処理、プッシュ通知 位置情報や加速度、カメラといったデバイスやセンサー類です。

そしてこれらの多くはModel領域の課題であるため MVVMでは解決できません

Why Prism for Xamarin.Forms

f:id:nuitsjp:20170315215735j:plain

Prismは単純にMVVMをサポートするだけでなく、こういった課題を解決するパターンやプラクティスが包含されてます。 そして、それらがアプリケーション全体のテスタビリティとメンテナンスビリティの向上に貢献します。 このため、BlueMonkeyではPrism for Xamarin.Formsを採用することとなりました。

MVVMのM

f:id:nuitsjp:20170315215736j:plain

とはいえ、Prismがすべてを解決してくれるわけではありません。
先ほどのモデルをもう一度見てみましょう。

MVVMが解決してくれるのは、Presentationの責務の分割だけです。
それ以外のすべての課題は未解決のままです。
そしてPrismもクロスプラットフォーム環境におけるDependency Injectionの機構などは提供してくれますが、Modelをどう作るかまでは規定していません。

それは個々のアプリケーションで決定する領域だからです。
とはいえ、異なるアプケーションであっても、共有できる考え方はもちろん存在します。

MVVMのM

f:id:nuitsjp:20170315215737j:plain

全てのアプリケーションで普遍的なモデルを構築することは困難でしょう。
しかし、モデルを分割設計し、実装・テストをどのように行うのか、その例を提示することはできると考えています。
この点が、BlueMonkeyの大きな目的の一つであると、私は考えています。

MVVMのMの原則

f:id:nuitsjp:20170315215738j:plain

さてMVVMパターンを採用する際、Modelの設計原則として広く共有されている考え方として次の2点があります。

ひとつ、ViewModelはModelの影であり、ViewはViewModelの影である。
つまり、プレゼンテーションというのはModelの影を映しているに過ぎず、本質はModelにあるという考え方です。

もうひとつは、Modelの呼び出しはメソッド呼び出しになりますが、そのメソッドの戻り値は、型パラメーターを持たないTaskクラスのみであり、メソッドの呼び出し結果は、変更通知という形でVMへ伝えるということです。

二つ目は少し分かりにくいですよね。

なぜ戻り値はTask縛りなのか?

f:id:nuitsjp:20170315215739j:plain

ここでは簡単にまとめますが、こちらに丁寧にまとめているので良かったらこちらもご覧ください。

www.nuits.jp

さて、なぜModelのメソッドは戻り値を持たないのか、簡単に説明してみたいと思います。
ここでは書籍ビューアを考えてみましょう。
書籍ビューアはドロワーに、書籍の目次を表示しています。
そして勿論、画面には本文が表示されいます。
このとき、本文のページめくりに対して、目次の選択されている章が同期され、同様に章が選択されたら、選択された章の先頭にページが飛ぶ。そんな書籍ビューアを想像してみてください。
目次と本文は、別々のViewとして構築されます。このときそれぞれの操作結果が、戻り値として返却されると、View間の同期が難しくなりますよね?
「読書中の書籍というModelがあり、ある視点から見ると目次というViewとして表現され、別の視点から見ると本文のテキスト画面というViewとして表現される。」
こう考えると、戻り値はTaskである理由が自然と見えてくるのではないでしょうか?

また、Modelの特定の状態に対して、今現在興味を持っているのはもしかしたら一人かも知れません。
しかし、それは常に一人であるとは限りませんし、一人ではないステートについては戻り値で値を返すわけにはいきません。

モデルへの作用と、モデルからの反作用を単一ルールで整理しようとすると、モデルのメソッドの戻り値はTaskのみにすることが必要であるということです。

MVVMのM どう整理する?

f:id:nuitsjp:20170315215740j:plain

さて、ModelとViewModelの境界は、Taskのみを返すメソッドと ViewModelへの変更通知だけが存在するという点は、理解いただけたかと思います。

ただそれだけでは、やはりModelが大きすぎます。 幾つかの役割に分割した方が好ましいでしょう。

BlueMonkeyのレイヤーアーキテクチャ

f:id:nuitsjp:20170315215741j:plain

BlueMonkeyではModelを大きく二つ

  • ユースケース
  • サービス

に分割しています。

What is Usecase?

f:id:nuitsjp:20170315215742j:plain

このときユースケーストいうのは、ただの機能ではなく利用者に対して、アプリが提供する本質的な価値のことを指しています。
一般的にいう機能とは異なる点に注意が必要です。

例えば下の図をみてください。

これは経費登録の画面です。
経費を登録するには二つの画面があります。
経費の情報のエントリー画面と、レシートなどの撮影画面です。
機能というと、エントリー画面も撮影画面もそれぞれが一つの機能でしょうし、例えばカレンダーから日付を選択するといったことも機能のうちです。

しかし、それら単独では、利用者に対して何ら価値を提供しません。 全てを通して初めて本質的な価値を提供するといえるでしょう。この単位がユースケースです。

BlueMonkeyのレイヤーアーキテクチャ

f:id:nuitsjp:20170315215743j:plain

さてもう一度レイヤー構成を見てみましょう。
BlueMonkeyでは、経費登録・経費閲覧・レポート登録・レポート閲覧といったユースケースが存在します。
ユースケースには、画面遷移感をまたがった状態管理や、ドメインつまりビジネスのロジックそのものをカプセル化します。
ユースケースはサービスを利用して実現します。
ユースケースはステートフルに作成します。

サービスは、ユースケースを実現するための個別の機能を提供します。
Azureのバックエンドとの通信サービスやログイン、写真撮影やローカル保存されている写真の選択などです。
サービスはステートレスに作成します。

しかし、この設計だと特定のケースでは役割が不足する場合があります。

オフライン同期を考慮する

f:id:nuitsjp:20170315215744j:plain

例えば、ネットワークが切断された状況でもアプリを利用可能にするオフライン同期の機能や通信内容のキャッシュ処理を実現したいといった場合が該当します。
バックエンドとの通信を考えた場合

  • オフライン同期やキャッシュの機構
  • 通信処理

これらの機能は分離することが好ましいでしょう。
そしてキャッシュとオンライン情報を制御して、オンラインかオフラインか意識せず、ユースケースに対して透過的に機能提供するレイヤーが欲しいように思います。

ただ、このあたりはバックエンドの製品選択と密接に関係してくるので、Azureだけではなく他の製品での実現方法も検討して、設計を汎化できないか考えたいと思っています。

Blue monkey コンポーネント図

f:id:nuitsjp:20170315215745j:plain

さて、それでは具体的にBlueMonkeyの物理的なコンポーネント構成を説明したいと思います。

BlueMonkeyコンポーネント図

f:id:nuitsjp:20170315215746j:plain

全体の構造としてはこのようになっています。
MVVMの各要素に該当するコンポーネントと、レイヤーに限定されない共通コンポーネントやプラットフォーム固有のコンポーネントに分離しています。

Platform別エントリーポイント

f:id:nuitsjp:20170315215747j:plain

まずは左上のiOSとDroidのコンポーネントを見てください。 これが各プラットフォームのエントリーポイントとなります。

Xamarin.Formsエントリーポイント

f:id:nuitsjp:20170315215748j:plain

これらから呼び出されるXamarin.Formsの共通のエントリーポイントとなる、App.csが存在するのがApplicationコンポーネントです。
App.csクラスでは、MVVM各層のクラスをDI Containerへ登録する処理が含まれているため、MVVMのいずれにも含まれない箇所に配置しています。

ViewとViewModel

f:id:nuitsjp:20170315215749j:plain

続いてViewとViewModelのコンポーネントです。
これらのコンポーネントはそれぞれ別のアセンブリに実装しています。
実のところ設計上はViewとそれに対応するViewModelはほぼ1:1になるため、必ずしも分割する必要はありません。
ただ、チーム開発する上では分離しておいた方が良いというのが、BlueMonkeyプロジェクトとしての考えです。
これはViewとViewModelを同一のアセンブリにしておくと、どこかで誰かが、ついうっかりViewModelからViewを直接操作してしまい、設計が崩れてしまうことがままあるためです。
そのための予防措置として分離した方が好ましいと考えています。
ちなみにVMを別プラットフォーム例えばWPFなんかと共有すること自体は、ぶっちゃけ厳しいです。

Models

f:id:nuitsjp:20170315215750j:plain

さて、続いてモデルです。
先ほども説明した通り、ユースケースとサービスに大きく分けられています。
実はこれがテスタビリティに対して大きく貢献しています。
プラットフォーム固有箇所や通信・時間といった概念をサービスに分離することで、ユースケースが非常に容易にテストできることになります。
この点については、後で実際にコードを見てください。

全体共通コード

f:id:nuitsjp:20170315215751j:plain

そしてBlueMonkeyアセンブリです。
ここにはプロジェクト全体で共通の概念を実装します。
一般的に考えられるのは、全体で共通で利用されるEnumなんかを定義すると良いでしょう。例えば性別などですね。
今回は、請求書やレポートといったエンティティに該当するクラスを定義していますが、もう少し大きなアプリになった場合、サービスやユースケースのインターフェース別に類似したオブジェクトであっても別々に定義した方が良いかと思います。

Usecase Transaction

f:id:nuitsjp:20170315215752j:plain

そして最後にトランザクションコンポーネントです。

Usecase Transaction?

f:id:nuitsjp:20170315215753j:plain

これはデータベースのトランザクションのことではなく、アプリ内のユースケースオブジェクトのライフサイクルを管理するために利用します。
今回のアプリケーションでは、DIコンテナにユニティを利用しています。
Usecaseは画面間でまたがって状態を共有できる必要があるため、ユニティを利用する場合、標準の仕組みではシングルトンを選ぶことになります。
しかしシングルトンの場合、ユースケース完了後、正しくオブジェクトを初期化しないと前の登録情報が意図せず引き継がれるような不具合を起こしてしまうことがあります。
そこで、ユースケースの終了時にコンテナ上のインスタンスを破棄し、ユースケース開始時にまた新しいインスタンスを割り当てることで、品質を確保しやすくなります。
ここは結構工夫した点なので、良かったら見てみてください。

コードをみてみよう!

てことで、あとはコードを見てほしいなと思います。

github.com

まだまだ、全然説明しきれていませんし、もう少し実装が進んで落ち着いたら正式なドキュメントも、がっつり記載する予定ですので、しばらくお待ちください。

という訳で、今日はここまで!