次の方法で共有


MVP を超えて

MVP パターンをエンタープライズ クラス アプリケーションの UI アーキテクチャ向けに拡張する

Haozhe Ma

モデル - ビュー - プレゼンター (MVP: Model-View-Presenter) は、UI のパターンについて考える際の糸口となるもので、UI デザイナーがアプリケーションの中で切り分けるべき懸案事項を明確にします。

しかし、MVP パターンにはさまざまな解釈があります。たとえば、MVP パターンは UI のアーキテクチャ パターンを明確に表していると思い込んでいる人もいますが、エンタープライズ クラス アプリケーションの場合、このような解釈はあまり当てはまりません。エンタープライズ クラス アプリケーションは、他の種類の UI アプリケーションに比べると、多種多様な要件に対処しなければならず、関与する関係者も多くなり、より複雑で、サービスや他のアプリケーションなどの別のシステムとの依存関係も多くなります。エンタープライズ クラス アプリケーションの UI アーキテクチャはこうした固有の特性により、柔軟性、管理の容易性、再利用性、実装の一貫性を高め、基になるテクノロジとビジネス機能とを分離して、特定の製品やベンダーに依存しないようにします。

エンタープライズ クラス アプリケーションの UI アーキテクチャ パターンとして MVP パターンだけを当てはめようとすると、いくつか疑問点が生じます。例を挙げてみましょう。

一般的なエンタープライズ クラス アプリケーションには多くのビューがあり、あるビューで発生するイベントが他のビューに影響を与えることがあります。たとえば、ある画面でボタンをクリックするとポップアップ ウィンドウが表示され、別の画面のデータが同時更新されるような場合です。こうした画面フロー ロジックを制御するのはどのパーツでしょう。各ビューに対応するプレゼンターから制御するのでしょうか。

サービス指向アーキテクチャ (SOA) では、アプリケーション UI は一般にサービスから情報を取得します。たとえば、UI は、WCF サービスを呼び出してデータを取得するために、生成済みの WCF サービス クライアント プロキシを呼び出す必要があります。プレゼンターからこのサービス クライアント プロキシを直接呼び出すことは、適切なデザインと言えるのでしょうか。こうしたサービスが別のテクノロジで実装されている場合、またはサービス モデルが変更される場合、どのように UI アーキテクチャをデザインすれば、このような変更が UI の実装に与える影響を最小限に抑えることができるでしょう。

このように考えてくると、実装によっては、生成済みのサービス クライアント プロキシ モデルをアプリケーション全体で使用することも考えられます。そうすることにはリスクが伴うでしょうか。専用の UI モデルが必要になる場合、サービス クライアント プロキシ モデルと UI モデルとのマッピングを行うのはどのパーツでしょう。

こういった疑問点は新しいものではなく、このようなギャップを埋めるため、他にも多数のパターンが導入されています。たとえば、ナビゲーション フローを制御する役割を想定して、アプリケーション コントローラー (Application Controller) パターン (英語) が導入されました。

そこで、このような MVP を拡張するパターンを取り上げ、1 つのコラムにまとめると、UI アーキテクチャ デザインの全体像を把握するのに役立つと考えました。この問題をエンタープライズ クラス アプリケーションの観点から考えると、UI アーキテクトが UI デザインに必要な重要なパーツを認識し、UI アプリケーションの実装に役立つ、一貫性のあるパターンを定義できます。

この記事全体を通して "MVP パターン" という用語を使用していますが、実際には本来の MVP パターンは使用されなくなっており、現在は本来の MVP から変化した 2 つのパターンが使用されます。1 つはパッシブ ビュー (Passive View) パターンで、もう 1 つは監視コントローラー (Supervising Controller) パターンです。どちらのパターンにも当てはまるシナリオがあり、それぞれ長所も短所もあります。図 1 に示す UI アーキテクチャは、主にパッシブ パターンに基づき、このパターンから拡張しています。監視コントローラー パターンを基に UI アーキテクチャを構成できないわけではなく、個人的好みの問題です。

Architecture Based on the Passive View Pattern
図 1 パッシブ ビュー パターンに基づく UI アーキテクチャ

MVP パターンから拡張した UI アーキテクチャを構成するパーツをきちんと理解することから始めましょう。図 1 は、UI アーキテクチャに必要な主要パーツを示す概要図です。この図には、ビュー、プレゼンター、UI モデル、プロセス フロー コントローラー、サービス エージェント、サービス クライアント プロキシ モデル、サービス クライアント プロキシという 7 つの主要パーツがあります。

ビュー

基本的に、ビューはパッシブ ビュー パターンのビューの役割を踏襲します。ビューは、UI の表示レイアウトとプレゼンテーション固有のロジックを処理することから始まる、一連の単純な役割を担います。

ビューの 2 つ目の役割は、プレゼンターに向けてイベントを発生することです。この役割を担うには、複数の実装が必要です。まず、ビューに IView インターフェイスを実装する必要があります。プレゼンターのロジックに影響を与えないように (与えるとしてもごくわずかになるように) したり、単体テスト機能を提供したりするには、プレゼンターから IView インターフェイス経由でビューを操作するようにします。

次に、プレゼンターから操作できるパブリック プロパティを定義する必要があります。パッシブ ビュー パターンでは、ビューからプレゼンターにデータを渡すことはなく、プレゼンターがビューから目的のデータを選択します。このように実装すれば、ビューとプレゼンターをバインドするコントラクトが減り、ビューとプレゼンターの役割をより明確に分離できます。

ただし、ビューのプロパティで使用するデータ型について 1 つ疑問が残ります。ビューのプロパティを定義する場合、文字列型や整数型のような単純データ型のみを使用するのが理想です。しかし、実際の実装では、ビューのデータを定義すると、面倒なことになります。UI モデルの定義など、複合型を参照して、データ プロパティを公開するのが妥当です。このようにして、アーキテクチャの純粋性を保つことと、実装の懸案事項を解決することのバランスを取ります。

3 つ目として、ビューでイベントが発生したときは、ビューからプレゼンターの操作を呼び出す必要もあります。ビューは、データを渡さずに、プレゼンターを直接呼び出すことができます。ビューとプレゼンターは必ず対になっているため、ビューとプレゼンターのデザインは疎結合ではありません。1 つのプレゼンター操作を 1 つのビュー イベント専用にすることは適切なデザインです。

ビューの最後の役割は、プロパティ値の更新に対応することです。プレゼンターからプロパティを更新して、変更点を示します。ビューでは、このような変更に対応する方法を決定できます。データの変更を反映してビューを更新することも、処理を行わないよう決めることもできます。

プレゼンター

基本的に、プレゼンターはパッシブ ビュー パターンのプレゼンターの役割を踏襲します。ただし、この場合、プレゼンターではプロセス フローを決定しません。プレゼンターは、ビューからイベント要求を受け取ってコントローラーに向けてイベント要求をパブリッシュし、コントローラーが次のステップを決定します。プレゼンターではプロセス フロー ロジックを処理しないため、ビューから受け取ったイベント要求が他のビューに影響するかどうかを認識できません。そこで、プレゼンターはビューからイベント要求を受け取ると、コントローラーがこれらのイベント要求に応答して次のプロセス ステップを決定できるように、対応するイベントを即座にパブリッシュします。プレゼンターでは、コントローラーから指示されるまで、処理を進めてなんらかの操作を実行できるとは想定しません。

プレゼンターが操作を実行する必要があるかどうかは、コントローラーが決定します。コントローラーからプレゼンターの操作が呼び出されると、プレゼンターはサービス エージェント経由でデータを取得するといった処理を実行します。プレゼンターからサービスに対して処理を実行する必要があれば、その操作をサービス エージェント経由で実行します。そのためには、プレゼンターからサービス エージェントに必要なパラメーターを渡し、サービス エージェントから結果を受け取ります。

データの変更についてプレゼンターからビューに通知できるようになると、ビューのプロパティ値を更新してその旨通知します。その後のプロパティ値の表示方法はビューが決定します。既に説明したように、プレゼンターは、ビュー オブジェクトに直接アクセスするのではなく、IView インターフェイス経由でビューを操作します。ビューのインスタンスはプレゼンターの開始時に既にプレゼンターに渡されているため、プレゼンターには処理の対象となるビューのインスタンスが既に存在します。

最後に、プレゼンターは UI モデルにアクセスすることができ、後から UI モデルのデータにアクセスする必要がある場合は UI モデルをキャッシュに格納することができます。

プロセス フロー コントローラー

プロセス フロー コントローラーは、アプリケーション コントローラー パターンに似ています。異なる点は、ここで説明するプロセス フロー コントローラーの役割が、プレゼンターから発生した型指定イベントに基づいてプロセス フローを制御することだけという点です。つまり、プロセス フローが画面のナビゲーション フローに限定されず、イベント要求に関連する複数のプレゼンター操作の順序を制御する処理も含みます。

プロセス フロー コントローラーは、プレゼンターがパブリッシュしたイベントをサブスクライブします。また、プレゼンターがパブリッシュしたイベントにしか応答しません。プレゼンターがパブリッシュするイベントは、型指定されます。つまり、プロセス フロー コントローラーは汎用イベントには応答しません。

プロセス フロー コントローラーは型指定されたイベントにしか応答しないため、実際にはイベントが発生したときにプロセス フローは既に決っています。このため、プロセス フロー コントローラーのロジックが簡略化されます。プレゼンターからパブリッシュされる各イベントには、プロセス フロー コントローラーがプレゼンターの他の操作を呼び出すときに渡す必要のあるデータが含まれます。

プロセス フロー コントローラーは、プレゼンターおよび関連するビューのインスタンスをまだ呼び出していなければ、それらを呼び出します。相互参照の問題により、制御の反転 (IoC) が必要になります。また、これによって、プレゼンターとプロセス フロー コントローラーのデザインが疎結合になります。

UI モデル

基本的に、UI モデルはパッシブ ビュー パターンのモデルの役割を踏襲します。パッシブ ビュー パターンのモデルは、それほど多くの処理は実行せず、モデル構造定義を提供するだけです。また、「プレゼンター」で説明したように、プレゼンターがモデルの状態を管理します。

ここで単にモデルではなく、UI モデルと呼んでいるのは、サービス クライアント プロキシ モデルと区別するためです。

UI モデルは、UI アプリケーション ロジックの処理に適したモデル構造を定義します。UI モデル定義は、サービス クライアント プロキシ モデルとまったく同じように見えることがあります。ただし、場合によっては (特に UI が複数のサービス ソースから取得したデータを表示する必要がある場合)、サービス クライアント プロキシ モデルとは異なる UI モデル構造を再構築する必要があります。

サービス エージェント

サービス エージェントは、プレゼンターとサービス クライアント プロキシとの間の仲介役として機能します。サービスは、その名のとおりに Web サービスである必要はありません。データの提供やビジネス ロジックの実行を行うすべてのリソースを示しています。もちろん Web サービスにすることもできますが、単純にファイル I/O にすることもできます。

サービス クライアント プロキシは、Web サービス テクノロジでは特別な意味があります。ここでは、サービスのゲートウェイを表すためにサービス クライアント プロキシを使用しています。

サービス クライアント プロキシには、技術的に特殊な部分を実装します。プレゼンターの視点から見ると、データの変換方法や提供方法はわかりません。このような技術的に特殊な詳細は、サービス エージェント内に隠します。つまり、プレゼンターがサービス実装テクノロジの変更、サービスのバージョン管理、サービス モデルの変更などの影響を受けないように、サービス エージェント層が保護します。

サービス エージェントは、アクセスするプレゼンターの操作を提供します。この操作に複合型を渡す必要があれば、UI モデルで複合型を定義する必要があります。このことは、操作の戻り値の型にも当てはまります。次に、対応するサービス クライアント プロキシの操作にこれらの操作呼び出しを渡します。場合によっては、1 つのサービス エージェント操作から、複数のサービス クライアント プロキシ操作呼び出しが行われることもあります。

サービス エージェント操作は UI モデルで定義された複合型を受け取るため、サービス クライアント プロキシ操作の呼び出し時に、UI モデルからサービス クライアント プロキシ モデルにサービス エージェント操作をマップする必要があります。サービス エージェント操作からプレゼンターに結果を返す必要があるときは、サービス クライアント プロキシ操作から結果を取得した後に、サービス クライアント プロキシ モデルから UI モデルにサービス エージェント操作をマップします。

これは面倒な作業になる可能性があります。ただし、あるモデル構造から別のモデル構造に簡単にマップできるツールがあるため、こうしたデザイン作業は多くの場合 1 度限りの作業になります。

サービス クライアント プロキシとサービス クライアント プロキシ モデル

Web サービス テクノロジにおけるサービス クライアント プロキシは、サービスがリモートでホストされている場合でも、サービス クライアントにローカル アクセスできるようにします。ここでは、サービスのゲートウェイとして機能するサービス クライアント プロキシについて説明します。サービス クライアント プロキシ モデルは、サービス コントラクト モデル定義を表します。

サービス クライアント プロキシからサービスへの呼び出しを渡し、サービスの応答を返します。サービスを ASP.NET Web Services (ASMX) テクノロジまたは Windows Communication Foundation (WCF) テクノロジを使用して実装すると、サービス クライアント プロキシを自動生成できます。

サービス クライアント プロキシ モデルは、サービス コントラクト モデル構造定義を反映します。

実装例

図 1 に示した UI アーキテクチャを説明するために、Windows フォーム アプリケーションを使った実装例を見てみましょう。このサンプル アプリケーションは、このコラムのダウンロードに含まれています。

このサンプル アプリケーションでは、まず、地域の一覧を読み込みます。ある地域が選択されると、その地域に属する顧客を表示します。顧客が選択されると、時間の範囲を照会するウィンドウをポップアップ表示します。開始時刻と終了時刻が入力されたら、選択された顧客に関係する注文の一覧をメイン画面のデータ グリッドに表示します。

この中で地域の一覧を表示するシナリオを使用して、図 1 の UI アーキテクチャをサンプル アプリケーションに実装する方法を説明します。図 2 に、このシナリオの呼び出しシーケンスを示します。


図 2 UI 呼び出しシーケンス

メイン画面フォームが読み込まれるときに、まず、プレゼンターのインターフェイスを呼び出し、現在のビューのインスタンスをプレゼンターのコンストラクターに渡します。

private void MainView_Load(

  object sender, EventArgs e) {



  _presenter = new MainPresenter(this);

  ...

}

MainPresenter インスタンスを呼び出すと、MainPresenter のコンストラクターが、渡された MainView インスタンスを IMainView 型のプライベート変数に代入し、次にコントローラーのインスタンスを呼び出して、現在のプレゼンターのインスタンスをコントローラーのコンストラクターに渡します。

public MainPresenter(IMainView view) {

  _view = view;

  _controller = new Controller(this);

}

コントローラーのインスタンスを呼び出すと、そのコンストラクターは、渡された MainPresenter インスタンスを IMainPresenter 型のプライベート変数に代入します。このコンストラクターは、RetrieveRegions など、MainPresenter からパブリッシュされるイベントへの応答を準備するイベント ハンドラーも定義します。

public Controller(IMainPresenter presenter) {

  _mainPresenter = presenter;

  ...

  _mainPresenter.RetrieveRegions += (OnRetrieveRegions);

}

メイン画面フォームの読み込みイベントに戻り、プレゼンター オブジェクトを呼び出してから、地域を取得するためにプレゼンターを呼び出します。

Private void MainView_Load(object sender, EventArgs e) {

  ...

  _presenter.OnRetrieveRegionCandidates();

}

プレゼンターがこの呼び出しを受けると、地域を取得するのではなく、RetrieveRegions イベントをパブリッシュします。RetrieveRegions イベントは IMainPresenter インターフェイスで定義されていて、MainPresenter に実装されます。

public event EventHandler<RetrieveRegionsEventArgs> 

  RetrieveRegions;

  ...



public void OnRetrieveRegionCandidates() {

  if (RetrieveRegions != null) {

    RetrieveRegions(this, 

      new RetrieveRegionsEventArgs());

  }

}

コントローラー クラスでは、RetrieveEvents のイベント ハンドラーを登録しているため、RetrieveRegions イベントに応答できます。

private void OnRetrieveRegions(

  object sender, RetrieveRegionsEventArgs e) {



  _mainPresenter.HandleRetrieveRegionsEvent();

}

コントローラーでは、プロセス フローが MainPresenter に制御を返す必要があるかどうかを決定し、MainPresenter に地域の取得を続行するよう求めます。コントローラーが MainPresenter 以外のプレゼンターを呼び出す必要がある場合、Unity フレームワークを使用してそのタスクを実行できます。

MainPresenter の HandleRetrieveRegionsEvent 操作では、サービス エージェントを呼び出して地域を取得します。説明を簡単にするために、ここで示す例ではこのサービスを実装していません。ここでは、ダミー データを書き込んで、アプリケーションが機能するようにしているだけです。サービス エージェントから結果が返されても、MainPresenter から MainView にデータを渡していないことに注意してください。代わりに、MainView の RegionCandidates プロパティを更新します。

public void HandleRetrieveRegionsEvent() {

  RegionAdminServiceAgent agent = 

    new RegionAdminServiceAgent();

  List<Region> regionCandidates = agent.RetriveRegions();

  _view.RegionCandidates = regionCandidates;

}

MainView の RegionCandidates プロパティで、地域の表示を処理します。

public List<UIModel.Region> RegionCandidates {

  set {

    _regionCandidates = value;

    PopulateRegionCandidates();

  }

}

地域を取得して MainView に表示する処理の全体的な流れは上記のとおりです。単にサービス エージェントを呼び出して地域を取得する場合より多くの手順が必要になることは間違いありません。ただし、エンタープライズ クラス アプリケーションの観点から考えると、デザインが疎結合になるだけでなく、実装パターンの一貫性が高まります。これによって、開発チームが実施するメンテナンスや知識の伝達が大幅に簡単になります。

このコード例についてもう 1 つだけコメントしておくと、処理の全体的な流れは、最初の Windows フォームの読み込みイベントから始まります。実装がより高度になると、コントローラーから始まり、コントローラーで最初に読み込むフォームを決定します。

まとめ

今回のコラムでは、MVP パターンの拡張に基づく UI アーキテクチャ デザインの 1 つの手法を紹介しました。UI アプリケーションは複雑になる可能性があり、UI アプリケーションのデザインは多種多彩です。ここで説明した技法は、これら多くのソリューションの 1 つです。多くの状況で役立ちますが、実装する前に、要件を満たしていることを確認してください。

既にさまざまな UI フレームワークが市場に出回っていますが、これらの多くは MVP (モデル - ビュー - コントローラー)、または今回紹介した 2 つのパターン拡張として考案されたパターンに基づいています。最初の手順として、たとえば、ここで UI アーキテクチャを抽象化したように、これらのフレームワークで実装されている主なパーツを確認することをお勧めします。アーキテクチャを考えるうえでは、最初に全体像を考えずに実装の詳細に取り組むことは適切な方法ではありません。身近な問題についてアーキテクチャの観点から幅広く理解することから始めると、システム アーキテクチャの基本的な問題が解決されるだけでなく、繰り返し可能な熟慮されたデザインを踏襲することができます。

最後に、今回の例では、C# を使用してコントローラーの実装を作成しました。Windows Workflow Foundation などのプロセス フロー テクノロジを使用する方が、より柔軟なデザインと実装が可能なので望ましいかもしれませんが、どのような技術を実装の詳細に使用するかは、ここで説明した UI アーキテクチャの基になる原理には影響しません。

MVP パターンの詳細については、MSDN Magazine の 2006 年 8 月号の記事 (英語) を参照してください。

Zhe Ma は、メイン州ポートランドに本拠を置く Unum Group のエンタープライズ アーキテクチャのテクニカル アーキテクトです。連絡先は、zma@unum.com (英語のみ) です。

この記事のレビューに協力してくれた技術スタッフの Don Smith に心より感謝いたします。