初期実装ビューの設計
論理ビューで抽出されたオブジェクトを、どのようにコンポーネントに分割配置するか設計します。ここでいうコンポーネントとは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の利用も決定したので、併せてプロットしています。これでいったん、データビューの設計は完了です。