主旨
会社の部署異動に伴い、引継ぎを行うこととなった。
自身がこれまでに勉強してきたエンタープライズシステムのアーキテクチャを説明する。
全体的に、OOPとDDDが中心となる。
また、現在C# Asp.Net MVC CoreによるWeb開発を行っているため、DBアクセスはC#の実装に寄る。
表面上のアーキテクチャはMVCであるが、エンタープライズあるあるのドメイン(ビジネスロジック)の複雑性に対応し、ファットモデルを防ぐために、DDDを取り入れる。
また、今回MVCにおけるModelは、DDDに寄せてEntityと表記する。
(この記事では、Model≒Entityとする。)
編集履歴
- 2022/03/07 VablueObjectの説明を追記
- xxxx/xx/xx ViewModelによる説明を追記予定
全体図
View
MVCのV。ユーザーインターフェイスであり、webではhtmlである。
ユーザーインターフェイスからは極力ビジネスロジックを排除する。
Controllerで取得したモデルを表示する役割を担う。
Controller
MVCのC。通常のMVCであれば、この部分でDB呼び出しをかけて、Modelを扱う。
しかし、前述したとおり、純粋なMVCの登場人物だけでは、ドメインの複雑性に耐え切れず、ContorollerやModelが肥大化する原因となる。
そこで、DDDを取り入れ、ControllerはApplicationServiceのエントリーポイントとにとどめるようにする。
Repository
DBアクセスを担うクラス。
基本的に、Repositoryからの返り値は、参照系では、プライマリキーによって求まる単一のEntityか、そうでない場合は複数のEntityになる。
更新系/削除系では、Entityやキーを受け取って、更新/削除を行うことになる。
DBアクセス自体をクラスでラップすることで、ドメイン側(ApplicationService, DomainService)から、どのような実装でDBからデータを取得しているのかを隠ぺいすることが可能となり、可読性が上がる。
(C#でいうところの、SqlReaderのようなSQLを直接実行する低レイヤーのライブラリから、Entity FrameworkによるO/Rマッパーであるかはドメイン側に関係ない。)
RepositoryInterface
Repositoryの扱う側はドメイン側(ApplicationService, DomainService)になるが、ドメイン側はRepositoryを直接参照することはない。
必ず、Repositoryに対応するインターフェイスを設け、ドメイン側はそのインターフェイスに依存する。
上記により、実際のDBからの依存を排除し、インメモリーによるDBモックでもドメインのプログラムを動作させることができる。
上記を実現することが、ビジネスロジックのテスト自動化を可能とする。
「抽象に依存せよ。インターフェイスに対してプログラミングせよ。」
上記の言葉が、正しく認識できているかは、オブジェクト指向を理解しているかの一つの指標となる。
RepositoryInterfaceはOOPによるインターフェイスを理解する上で打って付けの存在でもある。
Model (≒Entity)
MVCのM。DBにおける、1テーブルの1レコードをオブジェクト指向のインスタンスで表したもの。
クラス内のプロパティがテーブルの列に対応する。
上級者向けであるが、DDDのよくある課題点として、データを画面に表示する際は、正規化されたテーブルを結合し、非正規的なデータが求められる。
これを1テーブル1エンティティのルールに縛られると、Entityを扱うロジックを記述するDomainServiceやApplicationServiceで大量のレポジトリを必要とすることになる。
このような事態を回避するため、画面に描画するための非正規型ReadModelと、更新が行われた時の正規型WriteModelのように、そもそもモデルを分けてしまう考え方もある。
詳しくはCQRSパターンを参照。
ValueObject
Entityが1テーブルの1レコードを表すとするならば、ValueObjectは、1テーブルの1カラムをインスタンスで表したものとなる。
DBの1カラムにおいて、stringやintなどのプリミティブ型では、ドメインを表しきれない場合に、ValueObjectとしてラップすることで、ドメイン処理の隠ぺいやビジネスロジックの散在を防ぐために使用する。
O/Rマッパーを使用する場合、プリミティブ型以外へのマッピングは、自身で定義を行わなくてはならず、実装のはまりポイントになりやすい。
Entity Frameworkにおいて、私自身が次のポイントではまったので、実装例で記載しておく。
class MyDbContext : DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 複合キーのマッピング
modelBuilder.Entity<Model>().HasKey(x => {x.Item1, x.Item2});
// ValueObjectへのマッピング
modelBuilder.Entity<Model>().OwnsOne(x => x.ValueObject)
.Property(p => p.Value).HasColumnName("DBColumnName");
// ValueObjectが複合キーの一部の場合のマッピング
modelBuilder.Entity<Model2>().HasKey(x => {x.Item1, x.ValueObject});
modelBuilder.Entity<Model2>().Property(p => p.ValueObject)
.HasColumnName("DBColumnName").HasConversion(x => x.Value, s => new ValueObject(s));
}
}
DomainService
基本的にビジネスロジックはEntityに記述するが、Entityだけでは作れないビジネスロジックが存在する。
それは、同一の複数のEntityによって判断されるビジネスロジックや、異なるEntityが絡むビジネスロジックである。
上記のようなビジネスロジックをDomainServiceに設けることで、人間が扱う高級言語による仕様を、そのままプログラムの仕様としてモデリングすることが可能となる。
私自身、ここがエンタープライズアプリケーションを実装する上で一番楽しい箇所である。
ApplicationService
RepositoryやEntity, DomainServiceで形成したビジネスロジックを呼び出すクラスである。
様々なビジネスロジックを組み合わせて、ユースケースを実現することが、ApplicationServiceの役割である。
また、同時にトランザクションの単位でもある。
Controllerと同様に、ビジネスロジックは記述しない。様々なRepositoryやEntity, DomainServiceをまとめて、呼び出しの調整をするだけである。
最後に
5年間、エンタープライズアーキテクチャの技術書を読んできたが、感じることは「銀の弾丸はない」ということ。
世界中のプログラマによって、より複雑になりつつあるドメインに対して、どのように保守性を保ちつつプログラムできるか、議論がなされ続けている。まさに、DDDはその最先端である。
このようなドメインの複雑性に対して、ここまで自身が考察できるようになったのも、ひとえに今まで携わっていたシステムのおかげである。
ドメインの複雑性に立ち向かうには、ある程度の技術が必要であり、その獲得にはある程度の学習が必須である。
しかし、一度その技術を身に着けたころには、複雑なドメインをどのようにモデリングしようかと、夢中になっているはずである。
ぜひとも、ドメインの複雑性の先に見える、プログラマとしての面白さに興味を持ってもらいたい。
参考図書
ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本
実践ドメイン駆動設計 (Object Oriented SELECTION)
オブジェクト指向でなぜつくるのか 第3版 知っておきたいOOP、設計、アジャイル開発の基礎知識