前提条件
本稿はWPFアプリケーションのアーキテクチャ設計について記載したものです。見積編を前提に記載しているので、まだ未読であればそちらからお読みください。
本稿にはサーバーサイドの設計も一部含まれていますが、見積編にも記載した通り、サーバーサイドについてはWPFアプリケーションを設計する上で、必要最低限の範囲に限定しています。サーバーサイドの実現方式は、オンプレ環境なのかクラウド環境なのか? といった容易さなどで大きく変わってきます。そしてWPFアプリケーションから見た場合には本質的な問題ではありません。サーバーサイドまで厳密に記載すると話が発散し過ぎてしまい、WPFアプリケーションのアーキテクチャにフォーカスすることが難しくなるため、あくまで参考程度にご覧ください。
また本稿ではAdventureWorks社の業務のうち、発注業務システムのアーキテクチャとなります。特定業務は発注業務に限定されますが、認証などの複数の業務にまたがったアーキテクチャの実現についても言及します。
本稿は以下の環境を前提に記載しています。
- Visual Studio 2022 Version 17.4.0
- Docker Desktop 4.14.0
- Docker version 20.10.20
- SQL Server 2022-latest(on Docker)
- ComponentOne for WPF
- SPREAD for WPF 4.0J
- Test Assistant Pro 1.123
- .NET 6.0.11
本稿のサンプルは .NET 6で構築しますが、.NET Framework 4.6.2以上(.NET Standard 2.0水準以上)であれば同様のアーキテクチャで実現可能です。ただし一部利用しているパッケージのバージョンを当てなおす必要があるかもしれません。
想定読者
次の技術要素の基本をある程度理解していることを想定しています。
- C#
- WPF
- Docker
- SQL Server
これらの基本的な解説は、本稿では割愛しますが、知らないと理解できないわけでもありません。また下記の2つも概要が理解できていることが好ましいです。
- Clean Architecture
- ドメイン駆動設計(DDD)
Clean Architectureについては、筆者のブログである「世界一わかりやすいClean Architecture」をあわせて読んでいただけると、本稿のアーキテクチャの設計意図が伝わりやすいかと思います。
ドメイン駆動設計の適用範囲については、本文内でも解説いたします。
アーキテクチャ設計の構成
過去に記載した「実践WPF業務アプリケーションのアーキテクチャ【概要編】 ~ マイクロソフト公式サンプルデータベースAdventureWorksを題材に」では、アーキテクチャをRational Unified Process(RUP)にて提唱された4+1ビューを用いて記載・説明しました。
ただ4+1ビューでは次のような点で設計が導きにくいと感じてきました。
- ドメイン駆動設計との統合
- 非機能要件
- データの永続化手段と利用方法
- バージョン管理やCI/CD
これらを踏まえると、次のような視点(ビュー)でアーキテクチャを表現するのが良いと考えています。
No. | ビュー | 説明 | おもなモデル |
---|---|---|---|
1 | ドメインビュー | 境界付けられたコンテキストとコンテキストマップを用いてドメインとコンテキストを設計する。 | 境界付けられたコンテキスト、コンテキストマップ |
2 | ユースケースビュー | アーキテクチャを決定するための代表的なユースケースを選択・設計する。 | クラス図、シーケンス図 |
3 | 非機能要件ビュー | アーキテクチャに影響を与える非機能要件を特定・設計する。 | クラス図、シーケンス図 |
4 | 論理ビュー | ソフトウェアの論理レイヤー構成を設計する。レイヤー内の代表的なオブジェクトを抽出し、依存関係を設計する。 | パッケージ図、クラス図 |
5 | 実装ビュー | 論理ビューで抽出された代表的なオブジェクトをコンポーネントに分割配置する。 | コンポーネント図 |
6 | 配置ビュー | システム全体の論理ノード構成と、ノード上へのコンポーネントの配置を設計する。 | 配置図 |
7 | データビュー | システムが扱うデータの永続化方法、利用方法を設計する。 | ER図、クラス図、シーケンス図 |
8 | プロセスビュー | 並行性やパフォーマンス要件で特別な検討が必要と考えられるアーキテクチャを設計する。 | アクティビティ図、シーケンス図 |
9 | 開発者ビュー | システムの開発プロセスやツールを設計する。バージョン管理、CI、自動テストなどを含む。 | 配置図、シーケンス図 |
ドメインビュー
アーキテクチャ設計書のドメインビューでは、境界付けられたコンテキストとコンテキストマップを用いてドメインとコンテキストを設計します。
予算編で仮の境界付けられたコンテキストを作成しましたが、まずは対応するコンテキストマップを作成します。コンテキストマップではコンテキスト間の関係の種類を明確にします。関係の種類には共有カーネルやカスタマー・サプライヤー、腐敗防止層などが含まれます。
関係の種類を考慮して、論理ビュー・実装ビュー・配置ビューを設計します。
ユースケースビュー
4+1ビューが提唱されたRUPにおけるユースケースビューには、すべてのユースケースやアクターの抽出を含む、ユースケースモデルの完成ととれる意図が含まれていると思います。
ただそこまでいくと実質的な要件定義となってしまいます。それがダメという訳ではないのですが、アーキテクチャ設計の一部として記載することに個人的には違和感があります。
これは要件定義のリーダーと、アーキテクチャ設計のリーダーが異なることも多く、プロセスとしても要件定義とアーキテクチャ設計は部分的にオーバーラップして実施されることも多いからです。
要件は要件で文書化し、その要件を満たすアーキテクチャを設計するという流れが個人的にはしっくりきます。まず利用者に提供すべき価値(機能・ユースケース)があり、その手段としてアーキテクチャがあるはずです。そのためドキュメント体系としては、アーキテクチャ設計書の前にユースケース定義書(要件定義書)があるべきだと考えています。
ただアーキテクチャ設計書のユースケースビューには、ユースケースの実現を設計する役割もあります。
そこでユースケース定義書(要件定義書)によって設計されたユースケースを実現する上で、アーキテクチャ的なパターンを絞り込み、パターン別の実現方法を設計する役割をユースケースビューに残すこととしました。
非機能要件ビュー
従来の4+1ビューでは、機能要件に対する実現は明確に記載されていましたが、非機能要件の実現が考慮しきれていませんでした。そこで、機能要件に対するユースケースビューと同様に、非機能要件に対する非機能要件ビューを追加しました。
非機能全体の定義は、ユースケースビューと同様に、非機能要件定義書のような文書で定義されている前提条件とします。非機能要件には、たとえば保守・運用に関する要件なども含まれます。そのため非機能要件ビューでは、定義ずみの非機能要件からアーキテクチャ上考慮が必要な要件を特定し、その実現方法を設計します。
論理ビュー
システムを実現するための代表的なオブジェクトを抽出し、それらを配置する論理的なレイヤー構成を決定します。
他のビューでも同様ですが、論理ビューに記載する内容は、論理ビュー内の設計ですべて完成させることはできません。とくにユースケースや非機能要件の実現を設計する中で新しいオブジェクトが登場してきます。そのため複数のビューを行ったり来たりしながら設計を進めていくことになります。
実装ビュー
論理ビューで抽出されたオブジェクトを、どのようにコンポーネントに分割配置するか設計します。つまりVisual Studio上のプロジェクト(.csproj)をどのように分割して、どのクラスをどのプロジェクトに配置するのか決定します。
.NETの場合、プロジェクトの分割によって厳密な依存関係を規定できるため非常に重要です。安易にプロジェクトを統合してしまうと、すぐに好ましくない依存関係を実装してしまいがちだからです。
ViewとViewModelで考えると非常に分かりやすいでしょう。
MVVMパターンで設計する場合、ViewはViewModelに依存しますが、その逆をしてしまうと循環参照になってしまいます。しかし細かな実装をしていると、ついついViewModelからViewを操作してしまいたくなります。それで正しく動作することもありますが、WPFの場合はListなどが仮想化されている関係上、Viewを直接操作してしまうと不具合のもとになることもあります。また単純に依存関係が双方向になると、コードが追いきれないきれいなスパゲッティなコードになって、後日のメンテナンスで苦労しがちです。
そのため依存関係を適切に制御するため、プロジェクトをどう分割するかは、非常に重要な設計になります。
配置ビュー
どのノードに、どのコンポーネントを配置するか設計します。このとき実装ビューで抽出したコンポーネントだけでなく、サードパーティのライブラリやランタイム・OS・ミドルウェアなども記載します。ノードは論理的なノードとして扱い、物理的なノード設計はアーキテクチャ設計とは別に設計します。物理的な設計はインフラの詳細な設計にフォーカスするためです。
データビュー
システムが扱うデータの永続化方法、利用方法を設計します。
- 永続化先はファイルシステムなのか? RDBなのか? NoSQLなのか?
- RDBだとしたらスキーマをどのように設計するのか? 接続時のユーザーはどのように割り当てるのか?
- RDBをどのように利用するのか? Entity Frameworkか? Dapperか?
そういった内容を設計します。データベース全体のER図のような、詳細な設計は含めず、別途データベースの詳細設計書などに記載します。
プロセスビュー
並行性やパフォーマンス要件で特別な検討が必要と考えられるアーキテクチャを設計します。ほとんどの場合は、.NET(async/awaitなど)やASP.NET Coreなどが担ってくれるため、それらを単純に使うだけなら特別な設計は不要です。
今回のケースではWPFのプロセスを起動する際のDependency Injection(DI)コンテナーの初期化に関連する設計が必要になります。
開発者ビュー
システムの開発プロセスやツールを設計します。
- IDEには何を使うのか?
- ユーザーの開発環境に必要なランタイムやミドルウェアはなにか?
- バージョン管理には何を使うか?
- Gitを使うとした場合、そのブランチ戦略は?
- Unit Testフレームワークには何を使うか?
- CIはどのように行うか?
- CI時の自動テストは?
そういった日々の開発者体験に直結するプロセスやツールを決定します。ある意味では開発者にとって一番大切な部分でもあります。
設計編の構成
前述の構成は、アーキテクチャ設計書としては読みやすいと思います。しかし実際にアーキテクチャ設計を行っていく場合、各ビューを頻繁に行ったり来たりしながら記述します。いずれかを先に完璧に書きあげるという訳には行きません。
たとえば論理ビューから実装ビュー・配置ビューは概ねその方向に流れて設計しますが、論理ビュー自体がユースケースビューや非機能要件ビューの設計に伴い頻繁に更新されるため、ウォーターフォール的な流れにはならず、インクリメンタルなプロセスになります。
本稿では「アーキテクチャ設計書はこうなります」という設計結果をお見せするのではなく、どのようにアーキテクチャを設計していくか解説したいと考えています。そのため設計書としてのアーキテクチャ設計とは、やや異なったアプローチで記載します。
そこで設計編では、次の構成で記載していきたいと思います。
-
前編
- 初期ドメインビューの設計
- 初期ユースケースビューの設計
- 初期非機能要件ビューの設計
- 初期配置ビューの設計
- 初期論理ビューの設計
- 初期実装ビューの設計
- データビューの設計
- プロセスビューの設計
-
後編
- 非機能要件の実現
- 代表的なユースケースの実現
- 開発者ビューの設計
前編
本稿、前編ではまずはざっくりしたアーキテクチャの概略を設計します。
この段階ではあまり正確なものを作ることに拘る必要はありません。正確なアーキテクチャはすべてのユースケースや非機能要件が実現されるまで完成しません。そのため、まずは後編に記載があるような代表的なユースケースの実現に着手できる状態とします。速度を優先し、正確性はある程度目をつぶりましょう。
これはいい加減で良いという意味ではありません。とくに類似のアーキテクチャに対する経験が多い方は、この段階でかなり正確な設計が可能です。ただ、悩んで何日も手が止まってしまうくらいなら、先に進めてからフィードバックすれば良いと思います。
後編
後編では非機能やユースケースの実現を設計します。
ユースケースの設計をしていく場合、机上ですべて設計するのは難しくて、仮実装しながら設計していくことも多いと思います。その場合に、認証やロギングの機能が実装されていないと、そもそもユースケースを実装できなかったり、エラーの解析が困難になったりします。そこで非機能のうち重要な部分を、ユースケースより先に設計します。
非機能要件は、アーキテクチャに影響があるすべての非機能要件について設計する必要があります。ただ紙面の都合もありますので、今回は普遍的に活用できそうないくつかの非機能に絞って設計したいと思います。
ユースケースの実現は、必ずしもすべてのユースケースを同じ粒度でアーキテクチャ設計書に記載する必要はありません。ユースケースをアーキテクチャ的な視点でパターン分けして、同一パターンの中から代表的なユースケースを選定します。その代表的なユースケースに絞って記載する形とします。
これらの中で、各ビューにフィードバックしていき、アーキテクチャ全体の精度を上げていきます。そして最後に開発者ビューを設計します。本稿の構成上最後に記載しますが、実際には最後に書かないといけないという訳ではありません。書けるタイミングで順次記載していき、開発上必要になるタイミングまでに完成させれば良いかと思います。
では、いってみましょう!
初期ドメインビューの設計
本章では購買ドメインのドメインビューを設計します。ドメインビューでは次の2つのモデルを設計します。
- 境界付けられたコンテキスト
- コンテキストマップ
境界付けられたコンテキストを利用して、購買ドメインを中心とみたときに、関連するドメイン・コンテキストを抽出して、それぞれのドメインがどのような役割を持つのか設計します。そこで抽出されたコンテキスト間の関係を、コンテキストマップをつかって設計します。
境界付けられたコンテキスト
予算編で記載したように、Adventure Works Cycles社全体の境界付けられたコンテキストは下記のとおりです。
Adventure Works Cycles社はワールドワイドな自転車製造・販売メーカーです。そのためビジネス全体をみたとき、コアとなるのは販売ドメインです。販売ドメインを提供するために、購買・製造・配送ドメインが支援します。
ただし本稿の開発対象は購買ドメインです。購買ドメインからみた境界付けられたコンテキストは下記のとおりです。
予算編で記載したものとほぼ同じですが、下記の2点を変更しています。
- 業務横断に共通する概念として、AdventureWorksドメインとコンテキストを定義
- 製造・販売コンテキストから認証コンテキストの依存線を削除
前者については、AdventureWorks全社に共通するオブジェクトを定義するコンテキストとして導出しました。基本的に企業にとってプリミティブなオブジェクトを定義し、複雑なオブジェクトはそれぞれの業務ドメイン内で定義することも検討してください。詳細はコンテキストマップの中で説明します。後者は、購買コンテキストに集中し、他のコンテキスト間の依存関係は意識しないようにするため、あえて削除しました。
コンテキストマップ
コンテキストが導かれたら、次はコンテキスト間の関係を整理します。ドメイン駆動設計のコンテキストマップを利用して整理したモデルが下記のとおりです。
コンテキスト間の関係を整理するためには、次の2つを明確にする必要があります。
- 関係の向き
- 関係の性質
矢印の向きが上流下流を表していて、矢印の向いている先が上流、矢印の根元が下流です。コアとなる購買コンテキストと、それ以外のコンテキストの関係について順に整理しながら、それらをどう考えればよいか説明していきましょう。
販売コンテキストと製造コンテキスト
たとえば購買コンテキストで他社の部品などを発注する場合、どれだけ発注するべきか判断するためには、販売情報が必要です。つまり購買コンテキストは販売コンテキストに依存します。そのため購買コンテキストが下流で、販売コンテキストが上流になります。
関係が決まったらその性質を決定します。ドメイン駆動設計では次のような関係の中から、いずれの関係に該当するか決定します。
- 共有カーネル
- カスタマー・サプライヤー
- 順応者
- 腐敗防止層
- 別々の道
- 公開ホストサービス
関係の性質はこれだけではありませんし、既存の性質で表現できない場合は、あたらしく定義してもかまいません。ただ多くの場合は上記のいずれかから選べば十分でしょう。さて、購買コンテキストと販売コンテキストを見た場合、どういった関係になるでしょうか?
購買コンテキストと販売コンテキストの開発は平行に行われます。スケジュールなどにつねに余裕があるとは限りません。あまり密に結合していると、販売コンテキストの変更に購買コンテキストが、追随できない可能性があります。またリリース後に販売コンテキストの改修が入った場合、購買コンテキストへの影響はできる限り限定したいところです。
そもそも購買コンテキストで必要な販売情報は、販売コンテキストほど詳細な情報は必要ありません。このような場合、販売コンテキストと購買コンテキストの「販売」オブジェクトは、別々に設計・実装したほうが良さそうです。
そのうえで、販売コンテキストで「販売」された場合、その情報を適宜変換して購買コンテキストに取り込み、購買コンテキストの「販売」オブジェクトとして扱うのが好ましいです。つまり「腐敗防止層」の関係とします。
製造コンテキストについても同様です。購買のためには、現在製造中の製品も考慮して必要な購買量を決定する必要があります。そのため製造コンテキストは購買コンテキストの上流となり、性質も腐敗防止層とするのが良いでしょう。
なお今回は購買コンテキストからみた関係性だけ記載しています。実際には逆から見たときの関係性も検討する必要がありますが、今回は購買ドメインにスコープを絞っているため、ここでの説明は割愛します。
AdventureWorksコンテキスト
さて、購買コンテキストと販売コンテキストにはどちらも「販売」オブジェクトが登場することを説明しました。販売オブジェクトは、一見共通のオブジェクトのように見えますが、購買コンテキストと販売コンテキストで必要になる属性や振る舞いが異なることから、それぞれのコンテキストに別々に定義することとしました。
しかし逆に直接共有したほうが好ましいモデルやコードもあります。そういったオブジェクトをAdventureWorksコンテキストに定義します。AdventureWorksコンテキストには、具体的には次のようなオブジェクトを定義します。
オブジェクト | 説明 |
---|---|
Date | 時刻を持たない年月日 |
Days | 日数 |
Dollar | 通貨(日本企業の場合はYenなど) |
Gram | 重量グラム |
DollarPerGram | グラム当たりの料金 |
これらは少なくともAdventureWorks内ではプリミティブなオブジェクトで、直接共有したほうが生産性も品質も高めることができます。これらのオブジェクトは、購買・販売・製造コンテキストの開発者で合意のもと協力して開発します。そのため「共有カーネル」という関係を選択しました。
これらのオブジェクトをドメイン駆動設計のValue ObjectやEntityとしてAdventureWorksコンテキストに実装します。複雑なオブジェクトは個別の業務ドメイン内に実装したほうが良いため、ほとんどはValue Objectになるでしょう。
ただこれらも、変更容易性を優先する場合は、上記のオブジェクトも業務コンテキストにあえて定義する方式も考えられます。生産性と変更容易性はトレードオフの関係になりやすいです。この辺りは共通部分に破壊的変更が入りやすいかどうか判断したらよいと思います。
なお共有カーネルのVisual Studio上のプロジェクトは、プロジェクト参照として実装するのが扱いやすいです。各ドメインで共有して実装していくためです。
認証コンテキスト
認証はとくにセキュリティ上、非常に重要なコンテキストになります。そのため個別のコンテキストで実装することはリスクが高く、慎重に作られたものを共有することが好ましいと判断しました。
ただAdventureWorksコンテキストのように複数のコンテキスト間で共有して継続的に開発するというより、共通の仕様を規定して作られたコンポーネントをそれぞれが利用するという形をとることとしました。そのため関係の性質としてはカスタマー・サプライヤーを選択しました。
カスタマー・サプライヤーのVisual Studio上のプロジェクトは、安定するまではプロジェクト参照として実装したほうが扱いやすいですが、実装が安定したら別のソリューションに移動し、NuGetパッケージとして参照することを検討しても良いでしょう。
初期ユースケースビューの設計
ユースケースとは、利用者にとってシステムを利用する価値を表し、1つ以上の機能の組み合わせによって提供されます。
ユースケースと機能は明確に異なります。たとえば注文を発注する際に、発送方法をプルダウンで選択するとします。これは明確に「機能」ですが、利用者の最終的な価値にはなりません。ユースケースとはあくまでも利用者に価値を提供するための、1つ以上の機能の集合を表すものとします。
私たちが実現すべきものは機能ではなく、ユーザーに提供する価値だと考えています。そのため機能ビューではなく、ユースケースビューとして扱います。
ユースケースビューは、ユースケース図を完成させることが目的ではありません。ユースケースは、ユースケース仕様書(一般的な機能定義書のレイヤーに類するドキュメント)で作成されているものとします。
ユースケースは、アーキテクチャ的な視点から見ると類似したものが多く、すべてのユースケースで、個別に実現アーキテクチャを設計する必要はありません。そのため、アーキテクチャ的な視点から同一のユースケースを、同一のアーキテクチャパターンとしてまとめます。そして同一パターンの中から、アーキテクチャ設計に用いる代表的なユースケースを選定します。
初期ユースケースビューの設計では、代表的なユースケースの選定までを行います。アーキテクチャ設計は、要件定義と並行で行われることが多いため、すべてのユースケースが揃っている必要もありません。ドメイン内でとくに重要なユースケースが導出されていれば問題ありません。重要なユースケースのアーキテクチャを設計しているうちに、揃ってくるでしょう。
ユースケースビューは、次のような表を用いると表現しやすいかと思います。
No. | ユースケース | アーキテクチャパターン | 代表 | 選定理由 |
---|---|---|---|---|
1 | 発注する | 基本パターン | ||
2 | 再発注する | 基本パターン | ✅ | ユースケース固有の複雑な参照処理とそれを表示する高機能なグリッド、エンティティのCRUDが含まれるため。実際にはUとDは含まれないが、アーキテクチャ的にはCと差異がない。 |
3 | ・・・ | ・・・ |
ユースケースを一覧として記載し、それぞれのユースケースにアーキテクチャパターンを割り当てます。代表的なユースケースは、パターンに必要な要素が含まれた最小のユースケースを選定することが好ましいでしょう。あまり単一のパターンに含める要素が多くなりすぎると、設計が難しくなるため、パターンを小さめにして数を多くした方が良いでしょう。
たとえば「再発注する」ユースケースには、複雑なクエリーやそれを表示する高機能なグリッド、発注データのCRUDが含まれます。実際には、CRUDのうちCreate(C)とReference(R)は含まれていますが、Update(U)とDelete(D)は含まれません。ただしUDはアーキテクチャ的にCと相違ないため、エンティティのCRUDはこのケースで満たすこととします。
しかし理想的にはこのパターンは大きすぎるとは思います。ユースケース関連と、エンティティ関連の2つに分けたほうが良いかもしれませんが、設計編の後編では「再発注する」ユースケースの実装を詰めていくことでアーキテクチャを設計します。
初期非機能要件ビューの設計
非機能要件ビューはユースケースビュー同様、非機能要件を定義するものではありません。非機能要件定義書などで定義された非機能要件のうち、アーキテクチャ上で考慮が必要な要件を明確にし、その実現方式を設計します。
非機能要件には、運用時の要件など、アーキテクチャ的に考慮が必要ないものも多く含まれます。たとえば障害発生時の対応可能時間(9時~17時)などです。そのため非機能要件から、アーキテクチャ的に考慮が必要なものと、必要ないものを非機能要件ビューで選定します。
アーキテクチャ上での考慮が必要かどうか検討した結果と、そう判断した理由を記載するのが非機能要件ビューの目的となります。
ここで非機能要件の定義について、詳しく記載することはかないませんが、たとえばIPAの公開している「非機能要求グレード」をベースに不足があれば追加していくと、扱いやすいのではないでしょうか。
非機能要件のアーキテクチャ上の考慮要否を制する場合、次のような表を用いると表現しやすいかと思います。大項目から指標までは非機能要求グレードのフォーマットに則っています。
大項目 | 中項目 | 小項目 | 指標 | 考慮 | 理由 |
---|---|---|---|---|---|
可用性 | ・・・ | ・・・ | ・・・ | ||
運用・保守性 | 通常運用 | 運用監視 | 監視情報 | 要 | WPF上でのエラーとトレース情報を適切にサーバーサイドにログとして保管する。例外時にも抜け漏れのないログ出力を実現する。 |
障害時運用 | システム異常検知時の対応 | 対応可能時間 | 不要 | 運用保守の体制にて実現し、アーキテクチャに影響はないため。 | |
セキュリティ | アクセス・利用制限 | 認証機能 | ・・・ | 要 | WPFアプリケーションの利用者を認証する。認証機能はドメイン共通のカスタマー・サプライヤー関係として提供する。そのため、本来は全機能共通のアーキテクチャ設計上で実施するが、便宜上本稿に記載する。 |
・・・ | ・・・ | ・・・ | ・・・ |
上記は非機能要件の一部の抜粋です。
考慮列には、非機能要件ごとにアーキテクチャ上の考慮が必要かどうか記載し、その理由を理由列に記載します。考慮の要否以上に、なぜ決定したかその理由の方が後々重要になるため、しっかり書き残しておきましょう。後編では、例外処理を含めたログ出力と、認証の実現方法を設計します。
初期の配置ビュー
予算編で記載したように、購買システムはクライアント・Web API・データベースの三層アーキテクチャを採用します。また購買システムは販売ドメインと製造ドメインに依存するため、それらのノードも意識する必要があります。
購買ドメインの開発が開始しているということは、販売ドメインと製造ドメインの予算も決定されているはずで、購買ドメインの予算編と同等のアーキテクチャは決定されていると想定します。
それらを配置図に起こすと次の通りです。
購買・販売・製造パッケージがあり、それぞれに論理的なノードと、ノード上に配置されるコンポーネントを記載しています。販売ドメインも製造ドメインも、購買ドメインと同様に、データの永続化にはSQL Serverを利用するものとします。
この時点でもう少し設計を詰められそうなのは次の2点でしょうか?
- 購買APIのアーキテクチャ
- 販売・製造ドメインとの関係性
購買APIのアーキテクチャ
購買APIをRESTでつくるのか?それとも別のものを利用するのか?といったアーキテクチャの選択は、この時点でできることが多いでしょう。逆にいうと、個別のユースケースによって変わるようなものではないとも言えます。
Web API実装の選択肢として、次のものを候補として検討します。
- REST
- gRPC
- GraphQL
結論としては、今回はgRPCを利用する想定で設計を進めます。理由はいくつかあります。
- 購買ドメインでは、「発注する」ようなRPC(Remote Procedure Call)のようなスタイルのメッセージがあり、RESTのようなリソース要求スタイルか、RPCスタイルかのどちらか一方に寄せることで設計を簡略化するとした場合、gRPCに寄せたほうが素直な設計に感じる
- 購買ドメインのWeb APIを直接外部に公開する想定はなく、RESTほどの相互接続性は必要ない(gRPCの相互接続性が低いわけでもないが、RESTほど一般的でもない)
- 同様にGraphQLほどの柔軟性も必要ない
- RESTとgRPCでは単純にgRPCの方が軽くて速いことが多い
- .NETにおけるgRPCではMagicOnionというOSSを利用することで、C#のインターフェイスベースでの設計・実装が可能で、RPCスタイルのデメリット(エンドポイントが分かりにくい)を解消できる
gRPCの実装にはMagicOnionを利用する想定ということで、モデルに追加します。
MagicOnionは、クライアントとサーバーでそれぞれモジュールが異なるので、そのとおり記載しています。
なお初期論理ビューの設計より先にこちらを記載したのは、Web APIを決定しておくことで、初期論理ビューで設計できる部分が増えるためです。
販売・製造ドメインとの関係性
購買ドメインでは、販売・製造ドメイン上のオブジェクトを、購買ドメインに同期する必要があります。たとえば購買数を決定するにあたって、販売実績を参照するといった内容を実現するためです。
販売・製造ドメインとの関係性からアーキテクチャを設計する際、次の点を考慮する必要があります。
- 販売・製造ドメインが上流である
- 販売・製造ドメインとの関係性は腐敗防止層である
販売・製造ドメインは購買ドメインの上流にあたります。そのため販売・製造コンテキストのオブジェクトを購買ドメイン側で解釈し、購買コンテキストのオブジェクトに変換して取り込みます。それらを販売・製造それぞれの腐敗防止層コンポーネントとして実装することにします。腐敗防止層コンポーネントはひとまず購買データベース上に配置しましょう。
腐敗防止層の実装の詳細は、ユースケースビューでユースケースを設計することで詳細化します。ただ腐敗防止層の実装はWPFのアーキテクチャから乖離しすぎるため、「2022年版実践WPF業務アプリケーションのアーキテクチャ」内では取り扱いません。
初期論理ビューの設計
ドメイン駆動とクリーンアーキテクチャ
さて、アプリケーション全体の構成を考えたとき、ドメイン駆動設計単独では、どのように論理レイヤーを構成し、どの役割のオブジェクトをどのレイヤーに配置するのか規定されていません。そこで活用したいのがクリーンアーキテクチャです。ドメイン駆動設計とクリーンアーキテクチャは非常に相性が良い設計手法です。
クリーンアーキテクチャではなく、通常の垂直型のレイヤーアーキテクチャを用いた場合、概ね次のようなレイヤー構成になります。
ドメインがインフラへのアクセス層に依存する形となっています。
基本的に依存関係は、重要度の低い方から、高い方に向いていることが好ましいです。これは重要度が低い箇所の変更影響を、重要度の高い箇所が受けないようにするためです。このあたりの課題は筆者のクリーンアーキテクチャの解説を一読いただければ、ご理解いただけます。
ドメイン駆動設計でもっとも重要なのはドメインになります。垂直レイヤーアーキテクチャを用いると、ドメインとリポジトリー実装の依存関係が、理想とは逆の方向になっているのが見て取れます。
下図は有名なクリーンアーキテクチャの図です。
クリーンアーキテクチャを採用してEntityをドメインに置き換えて見てください。
サークルの左から右に向かっている矢印が依存の方向です。ドメインがもっとも内側にあって、ドメインはいずれにも依存しておらず、垂直型より良い設計になっているのがわかります。
さてクリーンアーキテクチャの詳細は先の記事を見ていただくとして、クリーンアーキテクチャについて誤解されがちな、いくつかの点についてここでも補足しておきたいと思います。
クリーンアーキテクチャでもっとも大切なことは、アーキテクチャ上もっとも重要な要素を中央のレイヤーに配置して、依存関係はすべて外から内に向けるという点にあります。
ただしそうすると、ドメインからリポジトリーの呼び出しのような、処理の流れが内側から外側にながれる部分の実現が困難になります。そのため、右下の実装例のように制御の逆転を使います。処理の流れとしては内側から外側にながれますが、依存性は一貫して外から内側へ向かうようにしましょう、というのが上記の図になります。レイヤー数や登場要素を、上記の図の通りにしましょうというアーキテクチャではありません。
ドメインレイヤーの分割
ここで、あらためて境界付けられたコンテキストを見なおしてみましょう。
購買ドメインの上位にAdventureWorksドメインが存在します。AdventureWorksドメインは、認証・製造・販売ドメインからも利用される汎用的なドメインです。そのためEntityの部分を単純にドメインに置き換えるのではなく、AdventureWorksドメインと購買ドメインの2層に分けたほうが良さそうです。
また、最も外側のFrameworks & Driversレイヤーの要素は、実際に今回のドメインで必要となるものを記載しましょう。それらを反映した現在のレイヤーモデルは次の通りです。
レイヤーアーキテクチャにおける選択
さてレイヤーアーキテクチャには2つの選択肢があります。
- 厳密なレイヤーアーキテクチャ
- 柔軟なレイヤーアーキテクチャ
厳密なアーキテクチャを選択した場合は、直下への依存しか許可しません。柔軟なレイヤーアーキテクチャを選択した場合は、相対的に下位のレイヤーであれば依存(利用)を許可します。厳密なレイヤーアーキテクチャを採用した場合、レイヤーをまたいだ内側を利用したい場合、1つ外側がそれをラップして隠ぺいする必要があります。たとえば今回であれば、プレゼンテーションがドメインを利用する場合、つねにユースケースでラップして隠ぺいすることになります。
厳密なレイヤーアーキテクチャの方が、内側の影響を受けにくくなるため保守性が向上し、柔軟なレイヤーアーキテクチャは内側を隠ぺいするコードが必要ないため、生産性が高くなります。
結論から言うと、今回は柔軟なレイヤーアーキテクチャを選択します。大きな理由が2つあります。
- AdventureWorksドメインをラップしてしまうと、生産性や品質に対する影響が大きい
- リポジトリーの実装などを考慮すると厳密なレイヤーアーキテクチャでは実現できない
AdventureWorksドメインをラップしてしまうと、生産性や品質に対する影響が大きい
前述しましたが、AdventureWorksドメインには次のようなオブジェクトを定義します。
オブジェクト | 説明 |
---|---|
Date | 時刻を持たない年月日 |
Days | 日数 |
Dollar | 通貨(日本企業の場合はYenなど) |
Gram | 重量グラム |
DollarPerGram | グラム当たりの料金 |
AdventureWorksドメインにおけるプリミティブな型をValue Objectとして実装するため、これらを一々ユースケースでラップすると生産性が低下しますし、ラップミスの発生もあり得るため、品質も低下します。
そもそも厳密にした場合、AdventureWorksコンテキストを共有カーネルにした意味が無くなってしまいます。
リポジトリーの実装などを考慮すると厳密なレイヤーアーキテクチャでは実現できない
たとえば購買ドメインには購買先を表すVendorエンティティと、そのリポジトリーであるIVendorRepositoryインターフェイスを定義することになるでしょう。そして、VendorRepositoryクラスはゲートウェイに実装されます。
VendorRepositoryクラスからIVendorRepositoryインターフェースへの依存は、ユースケース層を跨いでいますが、さすがにここをユースケース層でラップするのは助長過ぎます。
というわけで今回は柔軟なレイヤーアーキテクチャを選択します。
Frameworks & Driversレイヤー
最も外側のFrameworks & Driversレイヤーは、アプリケーションから利用するフレームワークやミドルウェアのレイヤーで、購買ドメイン外のレイヤーです。
ユーザーインターフェースはWPF上に構築し、永続仮想としてはSQL Serverを利用します。またクライアントとデータベースの間にWeb APIを挟んだ三層アーキテクチャとしたいため、Web APIをMagicOnionで実現します。
クリーンアーキテクチャ本にも記載されていますが、抽象化とは具体化を遅延させるための手段でもあります。そのためこの時点で具体的なFrameworkやDriverを決定する必要はありません。
ただ現実的な話、開発がスタートしてアーキテクチャを設計する段階では、Frameworks & Driversレイヤーの実体は決定しているものが多いです。なぜなら見積に影響するため、見積時のアーキテクチャ設計で多くの場合、十分に検討した上で決定しているからです。
すでに実体が決定しているなら遠慮せずWPFやSQL Server、MagicOnionのように具体的な要素をプロットしましょう。そのことはアーキテクチャ設計を容易にする面もあるからです。
一番分かりやすいのはWPFでしょうか。WPFで実装する場合、とくに理由がなければMVVMパターンを採用するでしょう。MVVMパターンを前提に設計されたUIフレームワークだからです。
このように外側の詳細の決定によって、内側の設計が容易になることがあります。そのため最外周が決定した段階で具体的な名称を記載しておくと良いと思います。
繰り返しますが。これは抽象化を利用して具象の決定を遅らせることを否定するものではありません。
初期オブジェクトのプロット
さて、前述のレイヤーモデルではさすがにオブジェクトが少なくて、初期の実装ビューを作成(つまりコンポーネント分割)することも難しいです。これ以上は代表的なユースケースを設計してみないと設計できないかというと、そうでもありません。
- ドメイン駆動設計
- クリーンアーキテクチャ
- Web APIを挟んだ三層モデル
- UIはWPF
- Web APIはMagicOnion
ここまでは決まっています。となると、ユースケースに関係なく、ある程度はクラスを導出できそうです。
おそらくユーザーが何らかの操作をした場合、次のような振る舞いになるはずです。
- ユーザーがViewを操作する
- ViewはViewModelを呼ぶ
- ViewModelはリポジトリーを呼び出してエンティティの取得を試みる
- クライアントからgRPCを利用してサーバーサイドを呼び出す
- サーバーサイドはリポジトリーの実装を呼び出してエンティティを取得する
これが正しいとして代表的なオブジェクトをレイヤーモデル内にプロットしてみたのがこちらです。
青の破線は呼出し経路です。おおむね、先の手順の通りとなっているのが見て取れます。ポイントが何点かあります。
データベース操作はWeb APIノード上で実施されて、クライアントからは行われません。そのためViewModelからIVendorRepositoryを呼び出した場合、実際に呼び出されるのはVendorRepositoryではなくて、VendorRepositoryClientです。
VendorRepositoryClientは、内部でIVendorRepositoryServiceを呼び出します。IVendorRepositoryServiceはMagicOnionでgRPCを実装するためのインターフェイスです。VendorRepositoryClientが呼び出すIVendorRepositoryServiceの実体はVendorRepositoryServiceではなくて、MagicOnionによって動的に生成されたオブジェクトになります。ちょっと分かりにくいので、VendorRepositoryClientの抜粋コードを見てみましょう。
public class VendorRepositoryClient : IVendorRepository { private readonly MagicOnionConfig _config; public VendorRepositoryClient(MagicOnionConfig config) { _config = config; } public async Task<Vendor> GetVendorByIdAsync(VendorId vendorId) { var server = MagicOnionClient.Create<IVendorRepositoryService>(GrpcChannel.ForAddress(_config.Address)); return await server.GetVendorByIdAsync(vendorId); } }
GetVendorByIdAsyncメソッドを呼び出すと、コンストラクターでインジェクションされたMagicOnionConfigからIVendorRepositoryServiceを作成し、GetVendorByIdAsyncメソッドを呼び出すことで、ネットワーク経由でサーバーサイドを呼び出します。
IVendorRepositoryServiceとIVendorRepositoryをまとめて1つにしたくなりますが、IVendorRepositoryService側のインターフェイスがMagicOnionにガッツリ依存するため、それはできません。実際のコードを見比べてみましょう。
public interface IVendorRepository { Task<Vendor> GetVendorByIdAsync(VendorId vendorId); } public interface IVendorRepositoryService : IService<IVendorRepositoryService> { UnaryResult<Vendor> GetVendorByIdAsync(VendorId vendorId); }
戻り値がTask<Vendor>とUnaryResult<Vendor>で異なります。UnaryResultはMagicOnionで定義されている構造体です。これを統合してしまうと、ドメイン層がMagicOnionに依存する形となってしまうため、今回はあえて分けました。もちろんMagicOnionにガッツリ依存するリスクをとって実装を減らすことで生産性を上げるという選択もあります。ただそれをするなら、コード生成を活用して分けるが実装はしないという、良いとこどりを狙うほうが個人的には好みではあります。
なお、この時点で完全に正しい必要はありません。おおよそ正しそうな状態にもっていって、あとは後続の設計の中で精度を高めていきます。完全に正しいモデルでなくても、このレベルのモデルがあると後続の検討が容易になります。
さて、これ以上は実際のユースケースを設計しながら進めたほうが良いでしょう。ということで、初期の論理ビューとしては、いったんこの辺りとしておきます。
初期実装ビューの設計
論理ビューで抽出されたオブジェクトを、どのようにコンポーネントに分割配置するか設計します。ここでいうコンポーネントとはVisual Studio上のプロジェクト(.csproj)のことです。
論理ビューで抽出されたオブジェクトを、どのプロジェクトに配置するか決定することが実装ビューの設計です。
もちろんモノリシックなプロジェクトでも作れない事はありませんが、.NETの場合は名前空間スコープがないので、実装時にあやまった依存を防ぐためにはプロジェクトを分割する必要があります。そのため依存関係を制限したい単位でプロジェクトを分割する必要があります。
今回は次のような方針でプロジェクトを定義します。
- トップレベルの名前空間はAdventureWorksとし、AdventureWorksドメインのオブジェクトを定義する
- 購買ドメインの名前空間はAdventureWorks.Purchasingとし、購買ドメインのオブジェクトを定義する
- インターフェイスの実装クラスを別プロジェクトに分離する場合、インターフェイル名前空間+実装アーキテクチャ名前空間に定義する(例:IVendorRepositoryのDB実装の名前空間はAdventureWorks.Purchasing.SqlServer)
- MagicOnionのクライアントプロジェクトはFoo.MagicOnionになるが、それのサーバー側実装クラスを含めたプロジェクトは3のルールに従えないため、Foo.MagicOnion.Serverとする
基本的に名前空間の親子関係は、子は親の具体化で、親は子を抽象化したものを表すようにします。親子関係で概念が繋がらない形にならないように注意します。これは子の名前空間から、親の名前空間はusing宣言しなくても利用できる .NETの仕様にあわせます。
3は実装アーキテクチャじゃなくて、Repositoryを配置するプロジェクトなので、AdventureWorks.Purchasing.Repositoryのような名前も考えられます。しかし、実際にはDB操作する実装と、リモートを呼び出す実装の2つがあり、名前が被ってしまいます。またデータをクラウドに永続化する場合、業務のトランザクションデータはRDBに、画像のようなバイナリーデータはBLOBサービスに置くといった形で、リポジトリーの保管先が別アーキテクチャになる可能性もあります。そのため、具象クラスを実装するコンポーネントの名称には、実装するアーキテクチャを表す名前にしておいた方が良いと考えています。
それ以外にも上記ルールだけでは上手くマッチしないことがでてくるため、適宜検討します。上記を基本として、論理ビューのオブジェクトをそれぞれのプロジェクトに配置した実装ビューが、次の通りです。
プロジェクト | 説明 |
---|---|
AdventureWorks | AdventureWorksドメインのドメインオブジェクトを含む |
AdventureWorks.Purchasing | 購買ドメインのドメインオブジェクトを含む |
AdventureWorks.Purchasing.View | 購買ドメインのViewを含む |
AdventureWorks.Purchasing.ViewModel | 購買ドメインのViewModelを含む |
AdventureWorks.Purchasing.MagicOnion | クライアントからサーバー上のリポジトリー実装を呼び出すためのMagicOnionClient |
AdventureWorks.Purchasing.MagicOnion.Service | リポジトリーをクライアントに対してgRPCインターフェイスで公開するWeb API実装 |
AdventureWorks.Purchasing.SqlServer | AdventureWorks.Purchasingで定義されたRepositoryインターフェースのRDB向け実装プロジェクト |
論理レイヤーモデルと並べてみると、正しく設計されているのが分かりやすいかと思います。
適切な単位でプロジェクト分割されていて、依存方向も問題ないことが見て取れます。
配置ビューの更新
さて、新しいコンポーネントが登場したので配置ビューを更新しましょう。
これで初期実装ビューの設計はいったん完了とします。
データビューの設計
データビューでは、データの永続化方法・利用方法を設計します。とは言えSQL Serverに永続化することは、予算編で十分検討して決定しています。
そこでこのビューでは、次の点について設計します。
- データベース利用アーキテクチャ
- ORMの選択
データベース利用アーキテクチャ
さて、みなさんはデータベースオブジェクトを配置するためのスキーマや、データベース接続時のユーザーなど、普段どのように設計しているでしょうか?
本稿はWPFアーキテクチャの記事なので、あまり深く踏み込めませんが、私自身は次の2点を重要視してアーキテクチャを設計しています。
- データベースオブジェクトの変更影響を、データベーススキーマ上だけで正しく判断できる
- 他のユースケースによるオブジェクト(テーブルなど)の変更が、他のユースケースに波及しない
テーブル変更した場合の影響範囲を正しく把握しようとしたとき、C#のコードを精査しなくては把握できない場合、RDBと .NETのインピーダンスミスマッチが原因で非常につらい思いをすることになります。そのため、次のように設計することで基本的にデータベースのスキーマ上だけで影響範囲を特定できるようにしています。
- 接続ユーザーはユースケースごとに別ユーザーとする
- 接続ユーザーにはユースケースを実現する上で最低限の権限を付与する
また特定のユースケースの変更のため、テーブルに新しい列が必要になったとします。その際に、そのテーブルを参照している別のユースケースへの影響がでるのも大変つらいです。そこで次のように設計しています。
- テーブルは直接操作せず、ビュー越しに操作する
- ビューはまったく同じ構造でも、ユースケース別に作成する
- ビューはユースケース専用のスキーマ上に作成する
ビューをデータベース上の抽象化レイヤーとして扱うことで、物理テーブル変更の影響を最小限で抑えるようにしています。なおドメインも同様で、ドメイン単位のユーザー・スキーマを利用します。
ユーザーをユースケース別に切り替える場合、アーキテクチャ的に考慮が必要なのでここで記載しました。具体的には後述します。
ORMの選択
現在、.NETでRDBを操作する場合に利用するORMとしては実質2択でしょうか?
- Entity Framework Core(以後EF)
- Dapper
ドメイン駆動設計で永続化されるオブジェクトとは、ドメイン層のエンティティになります。エンティティの永続化にはEFを利用されている方が一定数いることは認識していますが、私個人としてはDapperを利用しています。最大の理由はドメイン層をフレームワーク非依存で実装したいためです。
あらためてレイヤーモデルを見てみましょう。
Entity Framework Coreを利用する場合、次のいずれかで実装する必要があります。
- DDDのEntity(上図のVendor)をEFのEntityとして実装する
- EFをゲートウェイ(上図のVendorRepository)の中だけで利用し、ゲートウェイ内でDDDのEntityに詰め替える
前者はドメインがSQLサーバーに依存してしまい、依存は外側から内側だけという大原則に違反してしまいます。これはデータベース設計の変更が、アプリケーション全体に波及する可能性があるということで、可能な限り避けたいところです。
後者はというと、DDDのEntityとEFのEntityの両方を実装してつねに詰め替えるひと手間が増えてしまいます。正直なところEFを使うメリットがほとんど失われてしまうように感じます。またDapperで直接DDDのEntityを生成することに比較して、CPUもメモリーも多く消費する点も気になります。
また前節に記載した「データベース利用アーキテクチャ」をEFで守ろうとすると、データベースファーストでEFを利用する必要があり、EFを最大限活用することもできません。
これは私にとって身近なシステムの特性の問題が大きいため、EFをコードファーストで利用できるような環境であれば、EFを選択することは十分にメリットがあるのではないかと思います。私はEFに十分習熟しているとは言いかねるので、詳しい方の意見も伺ってみたいところです。
とにかく今回はDapperの利用を前提とします。
データベース接続コードの設計
データベース利用アーキテクチャを実現しようとした場合、実装に少し工夫が必要です。またデータベース接続の実装時に、単純にDapperだけだと痒いところに手が「届かない」箇所がいくつかあります。
- IDコンテナー上で接続文字列を解決する方法が提供されていない
- データベース接続コードやトランザクション制御コードがやや煩雑になる
特に前者は大きな課題です。データベース利用アーキテクチャを実現しようとした場合に、複数のデータベースユーザーを使い分ける必要があります。もちろんユースケース別にASP.NETのプロセスを分けて、1つのプロセスで複数のユーザーを使い分けないという方法もありますが、アーキテクチャ的にそれを制約とはしたくありません。アーキテクチャ的には1プロセスNユーザーを可能にしておきたいです。
そこでデータベースを抽象化して、下図のような設計にしたいと思います。
Databaseパッケージに接続文字列の解決やデータベース接続、トランザクション制御を共通化して実装します。Databaseの機能的な実装はDatabaseクラスで実装しますが、これをそのまま使うと、同一のDIコンテナー上で異なるデータベースユーザーを使い分けられません。
そこでDatabaseクラスは抽象クラスにしておいて、直接利用できないようにします。その上で、ドメインやユースケース単位でDatabaseクラスの実装クラスを用意します。データベースのユーザー・パスワードは、このドメイン・ユースケース単位のDatabaseクラスで管理します。
上図では購買ドメイン(AdventureWorks.Purchasing.SqlServer)と再発注ユースケース(AdventureWorks.Purchasing.RePurchasing.SqlServer)でそれぞれDatabaseの実装クラスを用意しています。これらをそれぞれのRepositoryに注入(Injection)して、次のように利用します。
public class VendorRepository : IVendorRepository { private readonly PurchasingDatabase _database; public VendorRepository(PurchasingDatabase database) { _database = database; } public async Task<Vendor> GetVendorByIdAsync(VendorId vendorId) { using var connection = _database.Open();
Databaseの実装クラスでユーザーを指定して、DIをIDatabaseではなく、具体的なDatabase実装クラス(ここではPurchasingDatabase)を指定することで、ユーザーを適切に切り替えることが可能となります。
ドメインビューの更新
ところでDatabaseパッケージは、どこに実装されるものでしょうか? すでに気が付いている人もいるかもしれませんが、これは認証と同じ位置づけにあります。ということで、ドメインビューまでフィードバックする必要があります。
境界付けられたコンテキストに汎用データベースドメインを追加しました。
そしてコンテキストマップに、カスタマー・サプライヤーとして追加しました。こうやって設計とともにドメインモデルの精度を高めていくことをドメイン駆動設計では蒸留といいます。
論理ビューの更新
続いて論理ビューを更新します。
大きな同心円はもともと購買ドメインの実現を表現したものです。データベースドメインは別のドメインで、カスタマー・サプライヤー関係にあります。そのため別の円に切り出しました。
実際はデータベースドメインを、必ずしも書く必要はないと思います。というのは、たとえばこの図にDapperはどこに書くのか?それ以前に .NETの標準ライブラリに含まれるintやstringはどこに? と同じことです。フォーカスしているドメイン外のものを書き始めるときりがなくなるからです。
ただデータベースドメインは現在のところ書ききれますし、その方が分かりやすいので現時点ではこうしてあります。ごちゃごちゃして書ききれなくなったら、またその時に考えます。
実装ビューの更新
では、これらのオブジェクトをプロジェクトに配置しましょう。
DatabaseなどはAdventureWorks社のデータベースドメインなので、AdventureWorks.Databaseコンポーネントに配置しました。PurchasingDatabaseは、VendorRepositoryと同じAdventureWorks.Purchasing.SqlServerプロジェクトに配置しました。RePurchasingDatabaseは、新たにAdventureWorks.Purchasing.RePurchasing.SqlServerプロジェクトを作成して配置しました。
配置ビューの更新
さて、コンポーネントが新しく発生したので配置ビューも更新しましょう。
Dapperの利用も決定したので、併せてプロットしています。これでいったん、データビューの設計は完了です。
プロセスビューの設計
プロセスビューでは、並行性やパフォーマンス要件で、特別な検討が必要と考えられるアーキテクチャを設計します。ほとんどの場合は、.NET(async/awaitなど)やASP.NET Coreなどが担ってくれるため、それらを単純に使うだけなら特別な設計は不要です。
今回のケースではWPFのプロセスの起動のみを設計の対象とします。というのもWPFアプリケーションをGeneric Host上で利用したいためです。
Generic HostはASP.NET CoreなどでWebアプリケーションやWeb APIをホスティングするための仕組みです。Generic Hostでは多様な機能が提供されていますが、特に重要なのは .NET公式のDependency Injection(DI)コンテナーが含まれている点にあります。そのためモダンなサードバーティライブラリはGeneric Host前提のものが多数ありますし、今後でてくる魅力的なライブラリもGeneric Hostを対象にリリースされるでしょう。
WPFの標準的な実装ではGeneric Hostにホストされていませんが、できないわけではありません。
フレームワークの選定
WPFをGeneric Host上にホストするライブラリは、次の2つがNuGet上に公開されています。
今回は後者のWpf.Extensions.Hostingを利用します。これはWpf.Extensions.Hostingをベースとしている画面遷移フレームワーク「Kamishibai」を利用したいためです。Kamishibaiは、次のような特徴をもつWPF用の画面遷移フレームワークです。
- Generic Hostのサポート
- MVVMパターンを適用したViewModel起点の画面遷移
- 型安全性の保証された画面遷移時パラメーター
- 画面遷移にともなう一貫性あるイベント通知
- nullableを最大限活用するためのサポート
たとえば画面遷移時に、文字列messageを引数として渡したいとします。その場合、遷移先のViewModelを次のように実装します。
[Navigate] public class FirstViewModel { public FirstViewModel(string message) { Message = message; } public string Message { get; } }
すると専用の画面遷移メソッドが自動生成され、次のように呼び出すことができます。
await _presentationService.NavigateToFirstAsync("Hello, KAMISHIBAI!");
画面遷移でパラメーターの型不一致が発生したり、デフォルトコンストラクターが前提とならないため、null安全な実装ができる非常に強力な、現状もっとも理想的な画面遷移フレームワーク……だと思って私が開発したものです。自画自賛抜きで良くできていると思っているので良かったら使ってみてください。
日本語のドキュメントも十分に用意しています。
コンテナー初期化とViewの分離
通常WPFプロジェクトを作成すると、アプリケーションのエントリーポイントとXAMLは同じプロジェクト内に作成されます。しかしこの設計には大きな問題があります。
アプリケーションのエントリーポイントでは、Generic Hostを初期化してアプリケーションをホストします。このときGeneric Hostで利用するDIコンテナーの初期化を行う必要があります。DIコンテナーの初期化を行うという事は、エントリーポイントからはソリューション内のすべてのプロジェクトを参照できる必要があります。
そのため、DIコンテナーの初期化とXAMLを同じプロジェクトに配置すると、XAMLから本来は触る必要のないオブジェクトを操作できるようになってしまいます。これを防ぐためには、エントリーポイントとViewのプロジェクトを分離する必要があります。
また実のところ、Web API側でも同じ問題があります。ASP.NET CoreのDIコンテナーの初期化処理と、VendorRepositoryServiceクラスを同じプロジェクトに配置すると、VendorRepositoryServiceから不要なオブジェクトを参照できてしまいます。そのためWPF・Web APIともに初期化処理を分離しましょう。これは実装ビューで表現します。
実装ビューの更新
更新した実装ビューは次の通りです。
WPFとWeb APIの初期化処理を実施するProgramクラスを含む、それぞれAdventureWorks.Purchasing.WpfとAdventureWorks.Purchasing.AspNetCoreというプロジェクトを追加しました。
またWPFで重要となるAppクラスをViewのプロジェクトに含めることを合わせて明記しました。テーマやスタイルなどを適用するためには、View側にないと不都合が多いためです。
配置ビューの更新
配置ビューも更新しましょう。
AdventureWorks.Purchasing.WpfとAdventureWorks.Purchasing.AspNetCoreを追加して依存関係を整理しました。
まとめ
さて、これでやっとユースケースや非機能要件を設計する準備が整いました。なかなか大変でしたね。ただ、ここまでの設計はシステムに依存しない部分が多いため、次回以降はそのまま流用できる部分が多いです。
今後、次のような内容で進めて、アーキテクチャ設計を完成に導きたいと思います。
- 代表的なユースケースの実現
- 非機能要件の実現
- 開発者ビューの設計
ここまでお付き合いありがとうございました。再びお会いしましょう!