はじめに
WPFにおけるXAMLの記述方法、各コントロールの使用方法、リレーショナルデータベースへの接続方法。そういった個別の技術要素に関する解説記事は多くあります。しかしそれら個別要素を組み合わせ、アプリケーション全体としてどう構築するかといった、全体のアーキテクチャに踏み込んだ具体例は少ないように思います。
その最大の要因は、アプリケーション全体のアーキテクチャは、ビジネスやシステム化の背景から強く影響を受けるため、正解はシステムの数だけ存在するからでしょう。また、同一ビジネス・同一システムであっても「正解」は一つではないでしょう。
このため、アプリケーション全体のアーキテクチャを、一般論として語ることが難しいというのが根底にあるのではないかと私は考えています。
とはいえ、その全体のアーキテクチャの設計こそが困難で、そして最も価値のある領域であるとも言えます。
そこで本稿では、特定のビジネス・システム化の背景を想定して、そのシステムを構築する上での実践的なアーキテクチャの一例をご紹介したいと覆います。
先にも書いた通り、同一の問題領域であっても、それを解決するアーキテクチャ的解答は一つではありません。人によっては、ここに記載するアーキテクチャに受け入れがたい要素が含まれることもあるでしょう。その事は私も否定するものではありません。
しかし本稿が、アプリケーションを構築する上でのアーキテクチャについて、悩んでいる人への一つのヒントになればと思い、執筆にチャレンジすることとしました。
なお、ここでいうアーキテクチャとは、ソフトウェア開発における重大な決定事項のすべてを指します。
本稿の内容について
本稿では、Microsoft社の公開しているSQL Serverのサンプルデータベース「AdventureWorks」を参考に、WPFで業務アプリケーションを構築するためのアーキテクチャを解説します。
本稿で解説するのは特に、リレーショナルデータベースを利用するWPFアプリケーションを、どう分割設計し、どう結合するかを主体として記載します。このため、各種の機能やコンポーネントの詳細な利用方法については、本稿での説明は割愛させていただきます。詳細な説明について、適宜外部のサイトを紹介させていただきます。
本稿は大きく次の流れで記載します。
- ビジネスの背景
- システム化における背景
- システム化の対象範囲
- システム化実現のアーキテクチャ設計
本稿で解説するアーキテクチャのプロトタイプは、GitHubに以下のアドレスで公開しています。併せてご覧ください。
- nuitsjp/AdventureWorks(GitHub)
それではさっそく始めましょう!
前提条件
本稿は以下の環境を前提に記載しています。
- Visual Studio 2017 Version 15.6.4
- SQL Server 2017 Developer
- SPREAD for WPF 2.0J
- .NET Framework 4.7.1
想定読者
次の技術要素の基本をある程度理解していることを想定しています。
- C#
- WPF
- SQL Server
これらの基本的な解説は、本稿では割愛します。
ビジネスの背景
以下は、AdventureWorksサンプルデータベースのビジネスシナリオとして公開されている文章の抜粋です。
AdventureWorksサンプルデータベースは、Adventure Works Cyclesという架空の大規模多国籍製造企業をベースにしています。この企業は、北米、ヨーロッパ、およびアジアのマーケットを対象に、金属製自転車や複合材製自転車の製造および販売を行っています。従業員290人の米国ワシントン州ボセルの拠点に加え、自社のマーケット基盤全体にわたって複数の地域販売チームを配置しています。
(中略)
Adventure Works Cyclesでは、昨年度の成功を基にマーケットシェアの拡大をねらっています。そのために、ターゲット顧客の絞り込み、外部Webサイトによる製品販売ルートの拡大、および生産コストの削減による販売コストの削減に努めています。
こういった背景のもとAdventure Works Cycles社では、次のビジネス領域における基幹システムを刷新することと決定しました。
- 販売およびマーケティング
- 購買
- 製造シナリオ
その結果、設計されたのがAdventureWorksサンプルデータベースです。
システム化の背景
本稿で対象とするのはAdventureWorksシステムの、各種マスターや従業員情報を管理するための「マスター管理」サブシステム(以降、本システム)です。
本システムは、販売や購買・製造といったビジネス領域で用いる別サブシステムと異なり、米国ワシントン州ボセルの拠点で勤務する290名の社員のみが利用します。利用者は全員、Windows 10クライアントを利用します。Windows 10端末はActive Directory(AD)に接続されており、ADにて認証情報は管理されています。
また、本システムの利用者は限定的です。このことから、開発および保守・改修に高いコストをかけることは難しく、とくに基本的なアーキテクチャや、基盤となる技術選択は、5年後に想定しているシステムリプレースまでの間、変更なく利用したいと考えています。
本システムはマスター管理機能を提供しますが、従業員の役割によっては頻繁に利用することが想定されます。このためシステムの操作性が業務の生産性に直結するため、高いユーザビリティが求められます。特にマスターメンテナンス画面では、可能であれば一覧で表示した中で、使い慣れたExcelライクな使用感で編集できることが好ましいと考えています。もちろんExcelと違い編集される値は不整合のない正しい状態を求められます。
これらから、比較的安定的な技術で構築された、ユーザー操作性の高いシステムが必要とされています。
また本システムで扱うAdventureWorksデータベースには顧客情報を含むセンシティブなデータが格納されています。このため十分なセキュリティが確保されている必要があります。
システム化の対象範囲
本システムはAdventureWorks基幹システムで利用するデータベース上の次のマスター管理を提供します。トランザクションデータに関しては、異なるサブシステム上で管理します。
本システムで管理対象となるマスターテーブルは次のとおりです。
-
Person.BusinessEntity
-
Person.Person
- HumanResources.Employee
- HumanResources.Gender
- HumanResources.MaritalStatus
- ...
なお本システムのスコープに入るテーブルですが、次のテーブルに関しては変更頻度が極端に低いことが想定されるため、システムとして提供は行わず定期的なメンテナンスにて直接データベースを更新するものとします。
- HumanResources.Gender
- HumanResources.MaritalStatus
- ...
AdventureWorks従業員管理システムのアーキテクチャ設計
それではさっそく、本システムを構築するためのアーキテクチャについて説明していきましょう。
システムを構築するための重要な決定事項(アーキテクチャ)と、その決定理由について解説し、必要と思われる個所では実際のコードを例にとって解説します。
本稿でアーキテクチャを記載するにあたり、Rational Unified Process(RUP)にて提唱された4+1ビューを用いて記載・説明していきます。
まずユースケースビューで、システム全体のユースケースから、アーキテクチャを決定するために重要となるユースケースを抽出します。
続いて、システム全体の論理構成を論理ビューで決定し、それを物理的にどう配置するか配置ビューで決定します。
その後、論理ビュー・配置ビューの決定事項に従い、抽出されたアーキテクチャ上重要なユースケースの実現方法を、実装ビューにて説明します。
最後にプロセスビューでは、並行性やパフォーマンス要件で特に重要と考えられる事項について検討しますが、本稿では非機能要求を規定することは難しいため、対象外といたします。
ユースケースビュー
本節では、システムのアーキテクチャを決定するために重要となるユースケースを選定します。
ユースケースとは、利用者にとってシステムを利用する価値を表し、一つ以上の機能の組み合わせによって提供されます。
ユースケースと機能は明確に異なります。例えばユーザーを登録する際に、性別をプルダウンで選択するとします。これは明確に「機能」ですが、性別をプルダウンで選択すること自体は、利用者の最終的な価値になりません。ユースケースとはあくまでも利用者に価値を提供するための、一つ以上の機能の集合を表すものとします。
本稿では、次のユースケースを対象にアーキテクチャを決定することとします。
アーキテクチャを決定する上で重要なユースケースは次の三つです。
- ログインする
- 従業員を閲覧する
- 従業員を編集する
ログインする
厳密に言うと「ログイン」はユースケースではありません。ユースケースを実行するための前提条件となる機能です。ログインは他のユースケースを利用するための前提条件であり、他のユースケースを構成する一つの機能に過ぎません。
しかしログインを他のユースケースの一部とした場合、すべてのユースケースで重複した記述(仕様や設計など)が発生してしまいます。また、ユースケースは機能ではありますが、アーキテクチャ上特別に重要な機能でもあります。
仕様書や設計書を記述していく上でユースケース駆動を採用した場合、本来はユースケースではないものでも、ユースケースとして管理したほうがプロジェクト全体で都合の良いものもあります。
このため、あえてユースケースとして定義し、必要となるアーキテクチャを検討するものとします。
ログインにおける認証には、統合Windows認証を利用します。本稿では認可については検討対象としません。
従業員を閲覧する
本システムの主となる機能で、従業員の一覧の表示、絞り込み、ソートといった機能を提供します。
管理対象とする社員は多くないため(現時点で290名)、すべての社員をグリッド上に表示し、その上でフィルターによる絞り込みや、ソートなどをExcelライクな操作感で提供することを望まれています。
従業員を編集する
「従業員を閲覧する」ユースケースを拡張し、グリッド上で表示している従業員の編集および新規追加を行います。
本ユースケースは、ユーザー インターフェースからデータベースまでの相互作用としては「従業員を閲覧する」と大きく異なりません。しかし、ユーザー インターフェースの実現にあたり、検討すべき課題が多くあるため、検討・記載することとします。
論理ビュー
システム全体レイヤーモデル
本システムは、次のとおりWPFによるWindowsアプリケーションをユーザーインターフェースとした、3層モデルを採用します。
3層モデルの選定理由
まず、クライアントにWindowsアプリケーションを採用した場合、3層アーキテクチャではなく2層のクライアント・サーバー型アーキテクチャも検討対象に上げられると思います。
ここでは、次の理由によって3層モデルの採用を決定しました。
- クライアント・サーバー型アーキテクチャではセキュリティ的な課題解決が困難
- トランザクション処理の実装は、クライアントの実装言語と統一したい
- データベースはスケールアウトが困難
まずクライアントから直接SQLでテーブル操作可能な形でデータベースを公開した場合、基幹システムであることからセキュリティリスクが問題であると考えました。
クライアントから直接データベースへ接続するためには、そもそもデータベースがネットワークレベルで接続可能な領域に公開されている必要があります。またクライアント・サーバー型アーキテクチャを採用した場合、データベースへの接続情報をクライアントアプリケーション内に含める必要があります。もちろん暗号化することは可能ですが、アプリケーションで復元できる以上、ユーザーが何らかの形で復元して接続情報を得ることを完全に防ぐことはできません。またデータベースの機能で、行レベルのアクセス制御を行うことは困難です。
クライアント・サーバー型で、なおかつセキュリティを担保してデータベースを利用するとします。その場合、SQL Serverであれば認証に統合Windows認証などを用いた上で、データベース操作は全てストアドプロシージャとして作成するといった形で実現は可能です。
しかしこの場合、クライアントの実装言語と、トランザクション処理の実装言語が異なってしまうため、要員のアサインなどが難しくなります。
また仮にSQL CLRを利用するなり、Transact-SQLに長けた人材が獲得できたとした場合でも、トランザクション関連の処理は全てデータベースサーバー上で実行されることになります。データベースはスケールアウトが困難なことから、データベースサーバーには可能な限りデータベースとしての処理以外は載せたくないとも考えました。
また、そもそもサーバーサイドの処理はデータベース操作のみとも限りません。
これらの複合的な要因から、3層モデルを採用することとしました。
WPFアプリケーションの選定理由
大く3点の理由があります。
- 高いユーザ操作性が求められている
- 利用者の端末がWindows 10で統一されている
- 技術的安定性が高く、リプレースまでアーキテクチャの維持が容易と思われる
Webアプリケーションを採用した場合、上記を満たしたうえで、高い開発生産性を発揮することは困難であると考えられるため、WPFアプリケーションに決定しました。
なお一般的にWebアプリケーションと比較してWindowsアプリケーションの場合、配布の側面で不利なケースも考えられます。しかし今回の場合は配布先が社内のWindows 10限定であることから、Click Onceを併用することで配布の利便性でも劣らないものと判断しました。
そもそも、個人的にはClick Onceが利用可能な状況下で、あえて業務システムをWebアプリケーションで作る理由は少ないと思っています。実際には顧客の(主に配布が困難であるという思い込みによる)要求でWebアプリケーションで構築するケースが多いでしょう。しかし今回はWPFを選択しました。
WCFサービスの選定理由
.NETアーキテクチャでトランザクション処理などをサービスとして公開する場合、代表的な選択肢として次の二つが考えられます。
- ASP.NET Web APIによるRESTfulサービス
- WCFによるRPCサービス
現在、一般的にはRESTfulサービスの方が主流です。もちろん、WCFでもRESTfulサービスは公開可能ですが、WCFは元々がSOAPベースのWebサービス構築のために設計されたものであり、ASP.NET Web APIの方が構造がシンプルでパフォーマンスも良好です。
しかし、ここではWCFをnet.tcpプロトコルで利用することとしました。主な理由は次の3点です。
- net.tcpを利用することでトランスポートレベルの暗号化・認証が利用できる
- バイナリフォーマットであるため高速なメッセージ交換が可能
- WCFは開発生産性が非常に高い
Web上に公開する場合、net.tcpの選択は困難ですが、イントラ上のオンプレミスシステムであればプロトコルとして採用に問題はありません。また接続のセキュリティ担保も容易であることから、net.tcpプロトコルによるWCFサービスを採用しました。
逆にインターネット上にサービスを公開するのであれば、現時点であればASP.NET Web APIが適切なケースが多いものと思います。
WPFアプリケーション層 詳細
さてWPFアプリケーション層のレイヤー分割についてですが、パターンAにするかパターンBにするか悩ましいところです。
まず、プレゼンテーションとドメインを分離(Presentation Domain SeparationつまりPDSを実現)するために、MVVMモデルを採用します。WPFは強力なバインディング機構をもちます。そのWPFアプリケーションでPDSを実現するために考えられたのが、MVVMモデルです。このためWPFとMVVMは非常に相性が良いパターンです。WPFでアプリケーションを構築するにあたり、あえて他のパターンを採用する理由もないため、MVVMモデルを採用することとしました。
ここまでは問題ありません。問題はModel層です。
私は通常は左のパターンAを採用するケースが多いです。これは一つのユースケースの実現に、二つ以上の画面が登場することを普段は想定しているからです。
一つのユースケースが複数の画面にまたがる場合、ユースケースの状態(情報)を管理するにはどうすれば良いか?という問題とユースケース内でロジックを共有化するのに、Usecaseレイヤーがあった方が都合が良いことが多いからです。
しかし本システムでは一つのユースケースが複数の画面によって構成されることは現時点では想定されていません。またユースケース固有の複雑なロジックも存在しません。非常にシンプルなマスター管理のCRUDが発生するのみで、それらは基本的にWCFによるサーバーサイドの実装に含まれます。
そこで本システムではパターンBを採用することとしました。
なおService Client層は、WCFサービスとの通信レイヤーです。実際にはWCFのシグニチャを定義したinterfaceを用いて、ChannelFactoryから自動生成するため実装は必要ありません。この点がWCFの開発生産性が高い所以です。
WCFサービス層 詳細
WCFサービスは、次のレイヤー構成にて実現します。
Service Host層は、WCFサービスの実装をホスティングする層になります。WCFをホスティングする仕組みには、二つの代表的な仕組みがあります。
- Windows Service上でのセルフホスティング
- IIS上でのホスティング
これらは機能的には大きな差異はありません。運用する環境において、Windows ServiceとIISのいずれでホスティングした方が、運用が容易になるかという側面で検討すると良いと思います。
今回のサンプルの実装においては、Windows 10上だとIISでホスティングする実装は難しいため、Windows Serviceとして実装しています。これはWindows 10上のLocal IISでは統合Windows認証が利用できず、IIS ExpressではHTTP・HTTPS以外のプロトコルが利用できず、IISのホスティングを利用する場合は別途Windows Serverが必要になってしまうことから、サンプル実装としては向かないという判断によるものです。
続いて、Service Implementation層です。これはWCFとして公開されるサービスの実装レイヤーになります。トランザクション管理を含むビジネスロジックが実装されます。実際のSQLの発行はDatabase Access層に移譲します。
ところでWeb上のWCFのサンプル実装を見ると、Service Host層とService Implementation層が一つに統合されている例が多く見られます。しかしその手法を取った場合、Windowsサービスでホストしていると劇的に開発生産性が落ちます。
開発時はWindowsサービスに登録するモジュールは、ビルド先のbinフォルダに配置されたものを登録したいと考えるでしょう。しかしそうした場合、Windowsサービスを止めないとビルドがエラーとなります。Windowsサービスが実行モジュールのファイルハンドルをロックしているためです。かと言って、Windowsサービスに登録するモジュールを別フォルダに置いたとしても、実行する際には毎回サービスを止めてファイルをコピーする必要があります。またWindowsサービスをデバッグするには、プロセスにアタッチする必要があります。F5を押して終わりという訳にはいきません。
また単純に、ユニットテストの対象コードと非対象コードが混在してしまうため、テストの終了判断が難しくなるという問題もあります。
これらの理由から、まずService Host層とService Implemantation層を分離します。その上で運用時はWindows Serviceに、実装時はWindowsコンソールアプリケーションにService Implemantation層をホストすることで全ての課題は解決することができます。
詳細はGitHubをご覧ください。
最後に、Database Asscess層ではデータベースへのSQLの発行と、結果のオブジェクトへの変換を実装します。トランザクション管理はService Implementation層で行うため、Service Implementation層で作成されたConnectionオブジェクトを利用して、データベース処理を実装します。
コンポーネントモデル
本節では、本システムで実装するソフトウェアのコンポーネント分割設計と、本システムの構築にあたり利用するコンポーネントを決定します。
本システムに登場するコンポーネントは次のとおりです。どのコンポーネントが、これまで説明したどのレイヤーに該当するのか、あわせて記載しています。
それぞれのコンポーネントの役割を以下に記載します。
コンポーネント | 役割 |
---|---|
SystemManager |
本システム内で横断的に利用される共有リソースを含みます。 具体的には従業員を表す値オブジェクトなどが含まれます。 |
SystemManager.Presentation | Presentation層のViewおよびViewModelを含みます。 |
SystemManager.Services |
WCFサービスのインターフェースを含みます。 インターフェースの引数や戻り値に登場するクラスなどは、SystemManagerコンポーネントに含むため、インターフェースのみが含まれます。 |
SystemManager.WindowsService | ServiceHost層のコンポーネントです。WCFサービスのロジックはSystemManager.Services.Impleコンポーネントで実装されるため、それをWindowsサービスとしてホストするための実装のみを含みます。 |
SystemManager.Services.Imple |
WCFサービスの実装コンポーネントです。 Servicesコンポーネントで定義されたインターフェースの実装クラスを含みます。 |
SystemManager.DatabaseAccess |
データベースに対するCRUDの実装するData Access Object(Dao)と、テーブルやViewと対になる値オブジェクトであるEntityクラスを含みます。 データベースへの問い合わせにはDapper及びDapper.FastCRUDを利用して行います。 |
Prism.Wpf | MVVMパターンのサポートライブラリであるPrismのWPF用実装コンポーネントです。ICommandの実装クラスや画面ナビゲーションの仕組み、Dependency Injection Container(以降DIコンテナ)のサポートなど、多数のベストプラクティスに基づく実装をサポートします。 |
Prism.Autofac | Pris.WPFにおいて、DIコンテナとしてAutofacを利用するための拡張ライブラリです。 |
Autofac | 本システムのWPFサプリケーションで利用するDIコンテナの実装クラスです。 |
ReactiveProperty | リアクティブ プログラミングをMVVMパターン上で、簡便に利用するためのライブラリです。 |
PropertyChanged.Fody | INotifyPropertyChangedインターフェースの実装を静的コード生成するためのライブラリです。 |
MahApps.Metro | WPFアプリケーションでMetroライクなモダンなユーザーインターフェースを手軽に実現するためのライブラリです。フラットなボタンやスイッチなどの基本的なコントロールも提供しています。 |
AutoMapper |
類似しているが異なるクラスにたいして、値をコピーする用途で利用します。 Database Access層では、データベースへ値を設定する際や、逆に値を取得する際、一旦System.DatabaseAccessコンポーネントで定義されているEntityクラスに保持します。 WCFサービスの引数や戻り値では、SystemManagerコンポーネントで定義された値クラスを利用します。 これら非常に類似したプロパティを保持しており、値の詰め替えが頻繁に必要となるため、本コンポーネントを利用して自動的に詰め替えを実行します。 またSystemManagerコンポーネントで定義された値クラスと、ViewModelの詰め替えにも一部利用します。 |
Catsle.Core | WCFサービスでAspect Oriented Programing(AOP)を行うために利用します。トランザクション制御や、認証処理を、サービスのメソッド群に横断的に適用するためにAOPを利用します。 |
SimpleInjector | WCFサービスで利用するDIコンテナの実装です。 |
Dapper | データベース操作を行うためのMicro ORMライブラリです。 |
Dapper.FastCRUD | Dapperのみでは提供されないCRUD操作をサポートする拡張ライブラリです。 |
各レイヤー別にコンポーネントを分割する理由について
各レイヤー間はすべて一方通行の依存関係にあります。
.NET Frameworkにはnamespaceによるアクセススコープは提供されておらず、代わりにアセンブリ別のアクセススコープ(internal)が提供されています。このため、複数のレイヤーのオブジェクトを同一のアセンブリに配置した場合、依存関係が双方向になることを防ぐことができません。
レイヤー間の依存関係が、一方通行となるように開発環境で担保するために、レイヤー別にアセンブリを分割することとしました。
ViewとViewModelを同一アセンブリした理由について
各レイヤーを異なるアセンブリに分割したのであれば、同様にViewとViewModelも分割するべきなのかも知れませんが、今回はSystemManager.Presentationアセンブリに一緒に格納しています。
最大の理由は、Prismの提供するVisual Studioエクステンションの機能を最大限に活用するためです。Viewを生成すると自動的に対となるViewModelを作成し、ViewとViewModelの紐づけが行われます。
このため、ViewとViewModelを別のアセンブリに分割することは妥協しました。
もちろん、その厳密性を優先するためにアセンブリを分割するという方針も、間違っていないと考えています。私自身、開発チームのメンバー構成や規模によっては分割することも多いです。
Prismの選定理由について
本システムではMVVMのサポートフレームワークとして、Prism for WPFを採用しました。Prismを採用した理由はいくつかあります。
- 現在も積極的に開発が継続されていること
- 日本語の情報が比較的充実していること
- 多数のベストプラクティスが含まれており、自然とそれらを適用できること
Prismはフルスペックに近いMVVMサポートフレームワークであり、テスト容易性や保守性を担保するためのガイダンスやベストプラクティスが多数含まれています。その点がPrismを選択した最大の理由です。
私自身、Prismのその方針に共感しており、ささやかではありますがPrismにContributeしています。
ただし逆に言うと、お仕着せ感の強いフレームワークでもあります。みなさんが各自で蓄積したノウハウがあり、そのためのライブラリやフレームワークをお持ちなのであれば、必ずしもPrismを選ぶ必要はないでしょう。
個人的にはMVVMに馴染みが薄いようであれば、まずはPrismを使ってみて、一通り理解した上で取捨選択することをお勧めいたします。
SPREAD for WPFの選定理由について
SPREAD for WPFを選定した理由の本質は、本システムでWPFを選択した理由と実のところ同じです。
- 高いユーザ操作性を低コストで実現可能
- 高い技術的安定性
WPFは非常に高い自由度をもったユーザーインターフェースフレームワークなので、突き詰めればすべて自作することで最高のユーザーインターフェースを実現できるでしょう。しかし常に無制限にコストや期間が掛けられるわけでもありません。
また、エンタープライズ向けのアプリケーションの場合、求められる操作性はある程度想定可能であり、それほど突飛なものは求められないことが多いでしょう。このため、ユーザ操作性と生産性を高いレベルで両立するために、サードパーティーのユーザーインターフェースコントロールを採用することは、非常に有効な手段です。
実際に少し、今回採用したSPREAD for WPFを見ていただこうと思います。
まず、グリッド上で表示・編集するオブジェクトとして次のようなシンプルな従業員クラスを用意しました。
public class Employee { public string FirstName { get; set; } public string LastName { get; set; } public DateTime Birthday { get; set; } public int Salary { get; set; } public bool IsManager { get; set; } }
これを次のように、SPREAD for WPFのグリッドへ単純にバインドします。行の追加だけプロパティで有効にしています。
<sg:GcSpreadGrid ItemsSource="{Binding Employees}" CanUserAddRows="True"/>
ではこのコードを動かしてみましょう。
EmployeeオブジェクトのListをバインドしただけですが、boolはチェックボックスに、DateTimeはカレンダー選択も入力も可能なコントロールに、数値は数値以外入力できず、入力中も適切にカンマフォーマットが適用されたテキストボックスになっています。
芸が細かいことに、日付入力で月の入力タイミングに5を入力しただけで、ちゃんと5月に確定されています。
もちろん、グリッドは細かい調整が可能です。通常のWPFのようにXAMLで記述したり、コードビハインドのC#コードでカスタマイズすることもできますが、専用のデザイナーで編集することも可能です。
次の画像がデザイナー画面になります。全ての列をソートとフィルタリング可能なようにカスタマイズし、Salary欄の入力値を0~1,000,000に制限するように編集しています。
SPREAD for WPFはExcelライクな操作感を提供するコントロールですが、デザイナー自体がExcelライクな操作感で、直感的に編集が可能です。ちょっとやりすぎなくらい、素晴らしい使い勝手だと思います。
実際に設定後の動作は次のようになります。
魅力的な機能が簡単に実現できていることが見て取れるでしょう。
本稿ではSPREAD for WPFについて、あまり深く踏み込んで解説しませんので、興味のある方は次の記事もご覧ください。
さてユーザ操作性と生産性の側面を見た場合、選択の候補としてはOSSプロダクトもあるでしょう。今回利用するのはグリッドのみで、OSSでもSPREAD for WPFに並ぶほどの機能性をもった製品は私は把握していませんが、もう少し一般的なコントロールであれば、OSSプロダクトも機能面では視野に入ってきます。
ただ、エンタープライズ領域の受託開発を想定した場合、個人的にはユーザーインターフェースライブラリは商用製品に比重を置きたいと考えています。
というのは、商用製品は対象バージョンの保守期限などが明確に定義されていることが多く、開発対象の保守計画をコントロールしやすいという面があるためです。
ではユーザーインターフェース以外はOSSで問題ないのか? となりますが、例えばDependency Injectionコンテナであれば、場合によって製品を変更したとしてもアプリケーション全体への影響は極小です。DapperやDapperの拡張ライブラリは十分に枯れていますし、仮に何らかのトラブルがあっても、自分自身で対処が可能なものが多いです。そもそも、これらの領域は対抗となる有償製品が無い、または少ないという根本的な理由もあります。
またエンタープライズ領域の受託開発では、開発メンバーや保守メンバーの継続的な維持が難しいという実情もあります。このため日本語のドキュメントや、日本語でのサポートが受けられるというのも大きな強みです。
ユーザーインターフェース以外のライブラリは、利用方法が限定的であるものが多いため、日本語の情報がある程度でそろっていることも多いですし、英語のドキュメントを読むにしても比較的容易でしょう。
逆に有償製品を採用する上での最大の難関は予算の確保です。
明らかに自作するよりも有償製品を購入することが安いとわかっていても、プロジェクトの予算が決定して走り出してからでは、有償製品を追加購入することは難しい現場が多いのではないでしょうか?
この課題は、顧客への見積提示前にアーキテクトが参画し、事前に予算を確保しておくことが最も確実な対処方法です。エンジニアが参画した時点で、アーキテクチャの大枠が決定済みで不自由な開発を強いられるといった話を稀に聞きますが、そういった意味でもアーキテクトは見積前の段階から参画することが重要だと、私は考えています。
話が少し発散してしまいました。
あらためて、SPREAD for WPFを選定した理由について振り返りたいと思います。
最も大きなポイントは次の二点です。
- 高いユーザ操作性を低コストで実現可能
- 高い技術的安定性
これに加えて、日本語のドキュメントが充実しており、サポートも日本語で受けることができる面も高く評価し、本システムではSPREAD for WPFを採用することとしました。
DIコンテナの選定理由について
今回、WPFアプリケーションではAutofacを、WCFサービスでSimpleInjectorを採用しています。本来であればこれらは、どちらかに統一すべきでしょう。
コンテナの機能だけを比較した場合、SimpleInjectorに旗が上がります。SimpleInjectorは後発であるため、作りが全体的にモダンであり、その結果動作が非常に高速です。そしてモダンなDIコンテナの中では、コミュニティが成熟しており今後のメンテナンスにも期待ができるように見えます。
詳細なDIコンテナの比較は、次の記事が参考になるでしょう。
そのためWPFもSimpleInjectorを選択したいところなのですが、現時点ではPrismがサポートしていません。
サポートしていないなら、自分でサポートを追加しても良いのですが、ちょうどタイミングが良くありません。
執筆中の2018年3月27日現在、Prismは7.0のリリースを間近に控えています。そして7.0ではDIコンテナ周りの実装が大きく変更になっています。現在リリースされているStableは6.3になりますが、それ向けにSimpleInjector対応を行うのは、すぐに無駄になる可能性が高いですし、7.0はまだDIコンテナ周りがフィックスしていないため、タイミングが早すぎます。
そのため、今回はAutofacを選択します。ただしPrism 7.0がリリースされたのちに、SimpleInjectorの採用をPrismコミュニティへ提案し、同意が得られるようであれば実装を提供しようと考えています。
もしPrismでSimpleInjectorが採用された場合には、DIコンテナの変更を検討したいと思います。なおDIコンテナの差し替え自体は、非常に容易に行うことが可能です。
Catsle.Coreの採用について
本システムではWCFサービスの認証の仕組みと、トランザクション制御にAspect Oriented Programing(AOP)を採用します。それらの機能は、WCFサービスで提供されるあらゆるAPIに横断的に適用する必要があります。こういった場合にAOPを採用することで、非常に簡潔かつ高品質な実装を行うことができます。
AOPの詳細を知りたい方は、以下の記事もご覧ください。Xamarin向けに書いた記事ですが、前半のAOPの解説についてはプラットフォームと関係ない内容になっています。
さてAOPを実装するにあたって、手段は複数ありますが、今回はCatsle.CoreのDynamicProxyを利用することとしました。理由は以下の通りです。
- .NET FrameworkのAOP実装として非常に多くのプロダクトで活用されている
- 動作が非常に軽快である
実装にはRefrection.Emitを利用した動的コード生成が利用されています。このため初回起動時にコンパイル処理が入りますが、以降は非常に高速に動作します。
Catsle.CoreとSimpleInjectorを組み合わせて利用する必要があるのですが、そのためには一工夫が必要になります。そこで簡単に利用するための外部ライブラリを作成しました。良かったらご利用ください。
代替手段として、静的コード生成を利用したCauldron.Interception.Fodyなどを利用する方法があります。こちらはコンパイル時にプロキシーが自動生成されるため、Catsle.Coreよりも高速に動作します。また静的にコード生成をしているため、例えばXamarin.iOSなどでも利用できる利点があります(Catsle.CoreはXamarin.iOSでは動作しません)。
しかしCauldron.Interception.Fodyは早いとはいえ、その理由はプロキシーの生成がコンパイル時に行われる点にあります。比較してCatsle.Coreは初回の実行時に動的にコード生成が行われるため、初回の処理が遅くなります。しかし今回の採用箇所はWCFサービスであり、一度起動した後は頻繁に再起動することは想定されません。したがってパフォーマンスによる差異は実質的にないと考えてよいでしょう。
また自由度の側面で見ると、Catsle.Coreに軍配が上がります。具体的にはCauldron.Interception.Fodyでは、メソッドの呼び出しを途中でブロックしたり、発生した例外をインターセプトしたりといった実装が不可能です。前者は例えばキャッシュの実装などが、後者はデッドロック時の自動リトライなどが考えられます。
こういった理由で、今回はCatsle.CoreのDynamicProxyを採用することに決定しました。
Cauldron.Interception.Fodyに興味をお持ちの方は、以下をご覧ください。こちらもXamarin向けの記事になっていますが、Cauldron.Interception.Fodyの利用方法自体はWPFでも完全に同じです。
SystemManagerとSystemManager.DatabaseAccessそれぞれの値オブジェクトについて
本システムでは、例えば管理対象の従業員を表すほとんど同じManagedEmployeeクラスが、SystemManagerコンポーネントとSystemManager.DatabaseAccessesコンポーネントの二か所に存在しています。
SystemManagerの値オブジェクトは、DatabaseAccesses以外の各レイヤーで共通クラスとして利用されます。SystemManager.DatabaseAccessesの値オブジェクトはデータベースのテーブルやビューと対になるクラスで、Dapperを通して利用します。
これらは多くの場合は全く同じプロパティを持ちます。
しかし、SystemManager.DatabaseAccesses側の値オブジェクトのプロパティは、場合によってはデータベースの製品に依存する型を取ることがあります。
またSystemManager側の値オブジェクトは複数種類のオブジェクトによるツリー構造をとることもありますが、SystemManager.DatabaseAccesses側のオブジェクトはそういった構造を取らないことが多いでしょう。
もちろん、ケースによっては全く同一のものしか発生しないこともあります。その場合は、一つだけ定義して共有すると良いでしょう。
しかし多くの場合は、無理をすれば共有化できないことはない。といった感じになります。その場合は、無理に統一することは必ずしも効率的になるとは言い切れません。
逆に、分けた場合であっても、増える手間はほとんど類似の値オブジェクトを二重で生成することと、値の詰め替えの手間が発生することくらいです。
前者については、今回はデータベースのテーブル構造から、Dapper.FastCRUD.ModelGeneratorを利用して自動的にSystemManager.DatabaseAccesses側のオブジェクトを生成するため、SystemManager側のクラスはそこからコピー&ペーストで生成すれば大きな手間は発生しないと考えています。
Dapper.FastCrud.ModelGeneratorについては、以下の記事もご覧ください。
後者、つまり値の詰め替え処理の手間については、AutoMapperライブラリを利用することで大幅に軽減されます。
例えば次のコードはSystemManager.DatabaseAccessのManagedEmployeeをSystemManagerのEmployeeに詰め替えているコードになります。
Mapper.Map<ManagedEmployee>(managedEmployee);
詰め替えの実装は大きなコストになりませんし、分離することで安定した構造を手に入れることができます。
このため本システムでは、SystemManagerとSystemManager.DatabaseAccessそれぞれの値オブジェクトを定義し、SystemManager.Service.Impleコンポーネントでオブジェクトの詰め替えを行うこととします。
Entity FrameworkではなくDapperを利用する理由
これはよく議論されている話題でもあります。私がDapperというかMicro ORMを利用する最大の理由は、Entity FrameworkのLINQでは記述できないSQLの実行が、エンタープライズアプリケーションでは避けられるとは限らない点にあります。
例えば特定条件による複数行のアップデートや削除などがそれに該当します。
もちろんEntity Frameworkでも生SQLの実行と同居することは可能です。しかしEntity Frameworkを通した処理で実行されるSQLは、ユーザー側で実行順序が制御できないという問題があります。例えば、
- Entity Frameworkで処理Aを実行
- 処理Aが実行済みの前提で、生SQLで処理Bを実行
みたいな処理をしたくとも、実際には実行順序が逆になってしまうといったことが起こります。この実行順序はEntity Frameworkでは制御できません。
また、Entity FrameworkのLINQは非常に便利なのですが、パフォーマンスを気にする場合に、LINQがどういったSQLを生成するか「忖度」してあげないといけない、なんてことも有りがちです。そもそもEntity Frameworkは、Micro ORMと比較して速いとは言えないという側面もあります。
業務アプリケーションの開発現場では、SQL(特にDML)を記述することは苦にならないというエンジニアが一般的でもありますので、エンタープライズアプリケーション開発の現場では、Micro ORMを利用したいというのが私の実情です。
その上で、どのMicro ORMを利用するか?となった場合、経験者の確保や情報収集の容易性を考慮すると、.NET Frameworkでは現在Dapperがデファクトスタンダードであり、多くの場合において適切な選択だと考えています。
Dapper拡張ライブラリの選定
さてDapperを利用するとした場合、素のDapperだけでは開発生産性が高くない(例えばPrimary Keyを指定して1行取得したいといった場合でもSQLをフルに書かないといけない)ため、拡張ライブラリの併用が好ましいです。拡張ライブラリを利用することで、Entity FrameworkのLINQとは言わないまでも、利用頻度の高いSQLはコードのみで表現することも可能になります。
Dapperの拡張ライブラリは多くあります。今回はDapper.FastCRUDを採用しました。DapperExtensionsなども選択肢としてはありだと思います。
詳細な比較は、以下の記事にも記載していますので良かったら、併せてごらんください。
配置ビュー
システム全体の配置モデル
本システムの最上位の3レイヤー、WPFアプリケーション・WCFサービス・データベースサービスを配置モデルで表現したものが以下になります。
システムの規模によっては、アプリケーションサーバーとデータベースサーバーは物理的に同一の筐体で提供することも可能です。ここでは物理サーバーについては言及しませんが、論理的に二つの役割をもったノードが必要になるものとしています。
また実際の構成では、データベースサーバーはフェイルオーバークラスター構成を、アプリケーションサーバーは負荷分散構成をとることになるでしょう。しかし、今回はソフトウェアアーキテクチャを対象としているため、詳細は割愛します。
詳細な配置モデル
各ノードに実際に導入されるOS・ミドルウェア・ランタイムそしてソフトウェアコンポーネントを図示したのが、次のモデルになります。
さて、よく見ると次の二つのコンポーネントだけ、利用者端末とアプリケーションサーバー上どちらにも配置されていることが見て取れると思います。
- SystemManager
- SystemManager.Services
SystemManager.ServicesはWCFで提供されるRPCのインターフェースが、SystemManagerにはそのサービスのシグニチャに登場するクラスが定義されています。この二つで、クライアント側とサーバー側の通信のルールが決定されていることになります。
WPFアプリケーションの配布について
WPFアプリケーションの配布はClick Onceを利用して行うこととします。
Windowsアプリケーションはユーザビリティが高く、その反面、配布や更新が困難だとよく言われます。しかしClick Onceを利用することでその課題はクリアすることができます。その際、アプリケーションを署名する証明書をクライアントへ配布する必要がありますが、Active Directoryで管理された企業内であれば、グループポリシーを利用して一括配布することが可能です。
中締め
ここまで、Microsoft社の公開しているSQL Serverのサンプルデータベース「AdventureWorks」を参考に、WPFで業務アプリケーションを構築するためのアーキテクチャについて解説してきました。次回はコードを交えながら、ユースケースビューで取り上げた代表的なユースケースと、アーキテクチャ上重要な要素について、具体的な実装方法を紹介します。