Windows Forms でのアーキテクチャー

当方がデータベースアプリケーションをWindows Formsで作成するときのアーキテクチャーを紹介します。

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

全体としてはModelとViewを分離するDocument-Viewを使用しています。Viewの役割を表示とキーボード・マウスなどのUIイベント処理に限定すると見通しが良くなります。それに伴い画面遷移などをModel側で処理するようになるとModelの役割が多くなり見通しが悪くなってきましたので、Model内の構造を見直す必要がでてきました。

そこでレイヤーアーキテクチャーを採用してModel内を分割することにしました。アプリケーション層・ドメイン層・データアクセス層に分割しています。アプリケーション層とドメイン層の間にサービス層を追加していたときもありましたが、ユーザの確認を差し挟む処理が書きにくいのでサービス層を無くしました。

コンポーネント 要約 コマンド実行 データバインディング その他 Reflectionでの分類
View データ表示、UIイベントの処理
  • UIイベントを受けてコマンド呼び出し。
  • ほとんどのコマンドはModelのメソッドを呼び出す。
アプリケーション層が提供するデータソースを入力・表示部品にバインディング。
  • Modelからの通知を受けて、画面遷移先のViewを生成
  • ボタン使用可・不可の連動、ダイアログの表示
  • その他UIデバイスの更新
メタレベル
Model アプリケーション層 (プレゼンテーションモデル) Viewの状態管理、コマンドの実行、ドメインの集約
  • Viewから呼び出されるコマンドに対応する処理を実行。
  • データ操作を伴わない画面遷移等はアプリケーション層で完結。
  • データ操作を伴うコマンドの具体的な処理はドメイン層に委譲。
  • 削除確認等処理に伴うダイアログ表示はアプリケーション層からViewへの通知。
  • 編集画面ではエンティティの変更追跡(Unit of Work)、トランザクションの管理を行う
データバインディングに必要なエンティティを集約し、データソース取得メソッドを通じて公開。 
  • 新しいModelのインスタンスを作成することで間接的に画面遷移。
  • Viewで使用するボタンの使用可・不可もここで管理
  • Viewへの各種通知
  • 画面毎にインスタンス生成
-
ドメイン層 (ビジネスロジック層) エンティティおよびビジネスロジックの提供
  • ドメインモデルおよびテーブルモジュールでロジックを記述。ルートエンティティのリストを集計する処理はテーブルモジュール
  • リポジトリにクエリーを記述。HQLのような抽象化はしていない。
  • 年次更新・インポート・エクスポートなどのバッチ処理、SQL使用
  • エンティティおよび所有する値オブジェクトがデータバインディングの対象となる。
  • アイデンティティ・マッピングを採用
  • エンティティ・値オブジェクトはコード生成。
  • マスタ類は全件読み込みしてリポジトリにキャッシュ。伝票類は画面毎にその都度インスタンス生成。
ベースレベル
データアクセス層 (パーシステンス層) データベースとの入出力
  • テーブルデータゲートウェイ形式でデータアクセス。
  • アクセス先の決定
  • DAOは不採用
  • 大方の機能はライブラリ化されているので記述量は少ない
メタレベル

レイヤー間の連係

各レイヤー間の連係は下図のようにしています。

データバインディングではViewからドメイン層のエンティティに直接バインディングしています。

コマンド実行では各層で必要があれば下層を呼び出しています。

各層は、Reflectionパターンのベースレベルまたはメタレベルで記述します。ドメイン層ではベースレベルで記述、すなわちエンティティのプロパティをそのまま使用します。Viewやデータアクセス層はメタレベルで記述、Viewでは入力部品やDataGridViewColumnがプロパティを表します。データアクセス層ではカラムオブジェクトがプロパティを表します。

データのロードはリポジトリを通じて行います。データベースの更新はUnit of Work によって行います。XMLやテキストファイルへの保存はリポジトリを通じて行います。

リポジトリにはクエリーを記述しメソッドによって呼び出せるようにします。伝票-伝票明細のような構造を持つ場合、関係表構造(IDataReader)からオブジェクトグラフへの変換(R/O変換)は汎用ライブラリを利用してリポジトリ内で行っています。

画面の継承関係

画面はメニュー・一覧・編集といった風にある程度パターン化しています。それぞれパターンには共通の処理がありますのでこれらをベースクラスに記述し、具象画面は派生クラスに記述します。Viewではもっぱらデザインでの継承階層、アプリケーション層ではメニュー・一覧・編集といった機能による継承階層を持たせています。

これによって各具象画面のコードは少なくなります。単純なCRUDだけですとView側に自分で記述するコードはありません。アプリケーション層もほとんど記述はありません。

画面遷移

画面遷移はModel側のアプリケーション層で管理します。ViewはModelに追従して画面遷移を行います。

下図でCallerModelは遷移元Model、CalllerFormは遷移元View、BaseModelはModelの共通の基本クラス(ここではstaticメソッドを表現)、CalleeModelは遷移先Model、CalleeFormは遷移先Viewです。

遷移元Modelが遷移先Modelの型とオプションを指定してstaticメソッドOpenModelを呼び、それに従ってCalleeModelを作成・初期化します。既に作成済みであれば再利用します。作成済みModelはグローバルなDictionaryによって管理しています。コンストラクタと初期化メソッドを分けているのはメソッドのオーバーライドによって初期化処理を柔軟に実装したいからです。

初期化が終わると、遷移先ModelのCallerプロパティに遷移元Modelを設定し、遷移先Modelでは遷移元ModelのAddCalleeを呼び出します。これを受けて遷移元Modelでは対応する遷移元Viewに対して、CallerAddedによって遷移先Modelが追加されたことを通知します。遷移元Viewは追加された遷移先Modelに対応する遷移先Viewを生成します。作成するViewは一定の規則でModel名からView名を導き出してリフレクションによって作成します。以上で遷移先ModelとViewの作成が完了し、次に遷移先ModelのRunメソッドを実行してModelとしての最初の動作を実行します。通常は対応するViewに対してShowを通知し、表示を促します。

このシーケンスですと画面間のデータの受け渡しは、Model同士で直接行いますのでViewを通じて受け渡すよりもシンプルになります。

参照