MVPパターンを業務アプリに適用する
業務アプリケーションを開発するにあたって、最低限守るべき事の一つとして「画面とロジック」の分離が挙げられると思う。
簡単そうに思えて、これが意外と難しい。
一人での開発ならば自分だけでやり方を決めてしまえばそんなに難しくはない。しかし、開発要員が増えれば、これが途端に難しくなる。
この原因としては、
- どこからが「画面」の役割で、どこまでが「ロジック」の役割かの定義が個々人でばらつきがある。
- 定義を決めても守らない奴がいる(そもそも意味が理解できていない)。
- 言語自体がへぼくて「画面とロジック」の分離がそもそも(充分に)できない。
なんていうのがある。
ほとんど、「それって開発者としてどうなの?」というレベルに思えるけど、現実の開発ってそんなもんだよね。
いつの時代もこの問題は付きまとうようで、実際色んなデザインパターンが考えられてきた。
知ってるやつでは、
- MVC(モデル・ビュー・コントローラ、Strutsとか)
- MVVM(モデル・ビュー・ビューモデル、WPF、Silverlight)
- MVP(モデル・ビュー・プレゼンター、後述)
- ドキュメント・ビュー アーキテクチャ(MFCのあれ)
なんてのがある。
他にも知らないだけで、いっぱいあるんでしょうね。
まぁ、そんなこんなで今回VB.NETを使って業務アプリを作る事になったんですけど、これがなんか20〜30人で開発するらしいんですよね(実装だけで)。
で、そこで開発サブリーダー的な役割を与えられるわけですけど、アサインする開発者の人たちはほとんど(というか全く).NETとか知らないわけですよ。ありがちな話ですけど。
だもんで、そのまま開発させたら画面のイベントハンドラにすべての処理を書いたりするわけですよ。
それを防ぐために何かしらのアーキテクチャが必要になってくるんですが、それに今回はMVPパターンを採用しようかと考えてます。WinFormアプリケーションなので、それとの相性とかも考えて、これが一番ベターかなと。
MVPパターンを実装するフレームワークとしてPatterns & Practiceの「Smart Client Software Factory」とかいうのがあるんですけど、試しにインストールしてプロジェクトを作ってみたら、山程ソースを生成しやがったので、こりゃ駄目だと思って採用は見送りました。
なので、単純なMVPパターンを実装するフレームワークを作ることにしました。
参考にしたのはこの辺
- C#と諸々 MVP (Model View Presenter)パターン
- http://blog.inomata.lolipop.jp/?eid=924433
- Enterprise Library 2.0を特徴づけるDI機能とは(3/3) - @IT
MVPパターンの特徴は、プレゼンターがビューとモデルの関連付けを行い、ビューとモデルはお互いに完全に独立しているという事。
このフレームワークで目指すところは、
- ビューやプレゼンター、モデルの関連付けは自動で行う。
- ビューの完全な分離(モデルやプレゼンターを参照させない、できない)
これをやるためにDIコンテナなんかの自動で関連付けを行うものが必要なので、「Unity Application Block」を使う事にする。
実装
前置きが長くなってしまったけど、実装を進めていく。今回、簡単なサンプルとして計算機アプリを作る。
画面イメージはこんな感じ。
画面イメージ
テキストボックスに数字を入れて、「計算」ボタンで計算結果を表示するだけのシンプルなもの。
とりあえず、これを実装したコードを先に紹介する。フレームワーク部分をのぞいたソースのみ。
まずは、実際の計算を行うCalculatorクラス。これが「モデル」
Calculator.vb
Public Class Calculator Function Add(ByVal value1 As Long, ByVal value2 As Long) As Long Return value1 + value2 End Function End Class
画面クラス。見てのとおり何もない*1。これが「ビュー」
MainForm.vb
Public Class MainForm End Class
そして、ビューとモデルを結びつける「プレゼンター」クラス。プレゼンターはPresenterBase(Of TView)(後述)から継承する必要がある。
MainFormPresenter.vb
Public Class MainFormPresenter Inherits PresenterBase(Of MainForm) Private _calculator As Calculator <Dependency()> _ Public Property Calculator() As Calculator Get Return _calculator End Get Set(ByVal value As Calculator) _calculator = value End Set End Property Protected Overrides Sub OnViewSet(ByVal view As MainForm) MyBase.OnViewSet(view) If view IsNot Nothing Then AddHandler view.btnCalc.Click, AddressOf btnCalc_Click End If End Sub Private Sub btnCalc_Click(ByVal sender As Object, ByVal eventArgs As EventArgs) With View .txtSum.Text = Calculator.Add( _ Integer.Parse(.txtLValue.Text), Integer.Parse(.txtRValue.Text) _ ) End With End Sub End Class
後は、イベントハンドラで画面から値を取ってきて、Calculatorオブジェクトで計算して結果を画面に設定している。
このプログラムのエントリポイントは以下のようにする必要がある。
Program.vb
Module Program Sub Main() Using dicon As New UnityContainer() dicon.AddNewExtension(Of PresenterExtension)() Application.Run(dicon.Resolve(Of MainForm)()) End Using End Sub End Module
UnityContainerをインスタンス化して、PresenterExtensionをAddNewExtensionメソッドで登録する。
後はResolveメソッドで画面をコンテナから取り出して、アプリケーションを開始するだけ。
これで「計算」ボタンが押されれば、プレゼンターのイベントハンドラが呼び出されて、そこからモデルが呼び出され、その結果がプレゼンター経由でビューに渡る事になる。
ビューが他の部分に一切関連していないのがわかると思う。
本来ならビューにもプレゼンターと関連付ける為のプロパティが必要になるけど、それをしたくない*2ので、UnityContainerの拡張機能を使って細工をした。
フレームワーク部分の解説
プレゼンター自体は何のひねりもない。
PresenterBase.vb
Public MustInherit Class PresenterBase(Of TView As Form) Private _view As TView Protected ReadOnly Property View() As TView Get Return _view End Get End Property Public Sub SetView(ByVal view As TView) _view = view OnViewSet(view) End Sub Protected Overridable Sub OnViewSet(ByVal view As TView) End Sub End Class
Viewプロパティ(Protected)とビューを関連付ける為のSetViewメソッドとそれを通知するOnViewSetメソッドがあるだけ。
細工はUnityContainerをインスタンス化した後にAddNewExtensionで追加したPresenterExtensionクラスにある。
UnityContainerはオブジェクトの生成プロセスにユーザが干渉できるようにする為の機構をいくつか用意していて、その一つとして「ストラテジ」というのがある。
これをUnityContainerに設定する為の窓口としてUnityContainerExtensionというクラスが用意されているのだ。
以下ソース
PresenterExtension.vb
Public NotInheritable Class PresenterExtension Inherits UnityContainerExtension Protected Overrides Sub Initialize() Me.Context.Strategies.Add( _ New PresenterInitializeStrategy(Me.Container), UnityBuildStage.Setup _ ) End Sub Public NotInheritable Class PresenterInitializeStrategy Inherits BuilderStrategy Private _container As IUnityContainer Private _presenterDefinitions As Dictionary(Of Type, Type) ''' <summary> ''' システム定義のアセンブリ名と思われる正規表現パターン ''' </summary> ''' <remarks></remarks> Private ReadOnly SysAssemblyPtrn As Regex = New Regex("^System.*|^Microsoft.*|mscorlib|vshost") Public Sub New(ByVal container As IUnityContainer) _container = container Initialize() End Sub Private Sub Initialize() ' 現在読み込まれているアセンブリからPresenterBaseを継承した型を検索する。 Dim types = AppDomain.CurrentDomain.GetAssemblies() _ .Where(Function(a) Not SysAssemblyPtrn.IsMatch(a.GetName().Name)) _ .SelectMany(Function(a) a.GetTypes()) _ .Where(Function(t) t.BaseType IsNot Nothing AndAlso t.BaseType.Name Like "PresenterBase`1") ' Presenterの型引数とPresenterの型でマップを作る。 _presenterDefinitions = types.ToDictionary( _ Function(tp) tp.BaseType.GetGenericArguments().First() _ ) End Sub Public Overrides Sub PostBuildUp(ByVal context As IBuilderContext) Dim view = context.Existing Dim viewType = view.GetType() If _presenterDefinitions.ContainsKey(viewType) Then Dim presenter = _container.Resolve(_presenterDefinitions(viewType)) presenter.SetView(view) ' コンテナに登録 _container.RegisterInstance(presenter) End If MyBase.PostBuildUp(context) End Sub End Class End Class
何をしているかというと、
- ExtensionのInitializeメソッドでPresenterInitializeStrategyをストラテジに追加する。
- PresenterInitializeStrategyの初期化時に現在読み込まれているアセンブリからPresenterBaseを継承している型を探して、その型パラメータと継承している型のペアでディクショナリを生成する。
- PostBuildUpメソッドでDIコンテナがオブジェクトを生成した後をフックして、そのオブジェクトの型が先程のディクショナリに存在すれば、対応するプレゼンターをDIコンテナから取り出す。
- プレゼンターにビューを設定する。
ということをやっている。
PostBuildUpメソッドの遅延バインディングでメソッド呼び出しをしている所は今のC#にはできない事なので、ちょっとだけVB.NETを見直した。
これだけでは現実の開発には適用できないので、まだ機能を作りこむ必要があるけど、基本的な考え方はまとまってきた。ビューがこれだけ分離してれば、画面とロジックを平行して作っていくのも楽になるんじゃないかな。
今考えている問題点としては、
- ビューに実装する機能はどこまでか?
- 画面の入力値チェック?
- 入力値チェックをするために必要な情報(DBとか)は、いつどうやって取得するか?
- 「計算」ボタンの前と後に画面側の処理を入れるにはどうするか?
- プレゼンターが画面のコントロールを直接参照しているので、画面側の変更をもろに受けてしまう。
- インターフェースを使って分離すべきか?
- インターフェースを使った分離はメンテナンスが面倒なので、あまりやりたくない。
と挙げればいくらでも出てくるけど、その辺もこれから考えていきたい。