この記事では、プログラムの設計や実装における原則や、デザインパターンや依存注入といった技術について説明していきます。
SOLID
SOLID(ソリッド)とは、ソフトウェア工学の用語で、オブジェクト指向における以下の5つの原理の頭文字です。
- 単一責任の原則 (Single-responsibility Principle)
- 開放閉鎖の原則 (Open/Closed Principle)
- リスコフの置換原則 (Liskov Substitution Principle)
- インターフェース分離の原則 (Interface Segregation Principle)
- 依存関係逆転の原則 (Dependency Inversion Principle)
単一責任の原則
単一責任の原則とは、コンポーネント(関数やクラス)が保持する責務は1つであるべきという原則です。 開発者の視点で言えば、依存を利用するクラスからその依存への制御を取り除くことで、依存を生成する責務がそのクラスから取り除かれて、開発者がその制御を得ることができ、結果として単一責任の原則の適用につながります。
開放/閉鎖の原則
開放/閉鎖の原則 (Open/Closed Principle; OPC) とは、既存のコードを変更することなく、アプリケーションを拡張できるようにするという概念のことです。 拡張に対しては Open であり、変更に対しては Closed すべきであると主張しています。 一見矛盾しているように見えますが、依存注入を使うことで、依存を置き換えたり介入したりするだけで、依存を利用しているクラスに影響を与えることなくアプリケーションの振る舞いを変えたり、加えたりすることができるようになります。 全てのコードをそのような状態にすることは不可能ですが、オブジェクト間が疎結合になるよう設計することで、この原則が目指していることに近づくことはできます。
関係する原則としてDRY (Don't Repeat Yourself) 原則があります。 DRY原則は、すべての知識はシステム内において単一かつ明確な信頼できる表現になっていなければならない、というものです。 つまり、開発者は各知識を複数の場所に散らばらせるのではなく、一箇所に集中するように努めなければなりません。 DRY原則は知識やドキュメントに注目しているのに対して、開放/閉鎖の原則はプログラムに注目しているという違いがあります。
リスコフの置換原則
リスコフの置換原則 (Liskov Substitution Principle) とは、インターフェイスを変えることなく様々な機能を使えるようにするというソフトウェア設計の考え方の一つです。 例えば、コンセントというインターフェイスは変えることなく、それを使った様々な家電製品を使うことができます。 ソフトウェアにおいて、インターフェイスを修正するときは、それを使うクラスや実装を破綻させることなく、別の実装に取り替えられなくてはなりません。
インターフェイス分離の原則
インターフェイス分離の原則 (Interface Segregation Principle) とは、不要なインターフェースに依存することを避けるべきという原則です。 すべての機能を1つの大きなインターフェースで提供するのではなく、クラスに必要なメソッドのみを提供するインターフェースを作成し、不要なメソッドを含まないようにすることが推奨されます。
依存関係逆転の原則
依存関係逆転の原則 (Dependency Inversion Principle) とは、次の2つの原則から成り立ちます。
- アプリケーションにおける上位モジュールは下位モジュールに依存すべきではなく、どちらも抽象(インターフェース)に依存すべきである
- 抽象は実装の詳細に依存すべきではなく、実装の詳細が抽象に依存すべきである
抽象型を利用しないと、上位レベルのモジュールが下位レベルのモジュールに直接依存してしまいます。 これはビジネスロジックを含む高レベルのモジュールが、低レベルの技術的な詳細に依存することを意味します。 本来主導権を持つべきなのはビジネスロジックを含む高レベルのモジュールです。 依存関係逆転の原則に従うことで、下位レベルのモジュールの変更が上位レベルのモジュールに与える影響を小さくし、システムの保守性を高め、変更を容易にします。
その他の原則
制御の反転
制御の反転 (Inversion of Control; IoC) とは、プログラムの流れを反転される原則のことです。 従来のプログラミングでは、アプリケーションコードがプログラムの制御の主体となり、必要に応じてライブラリやフレームワークのコードを呼び出します。 つまり、制御の流れはアプリケーションコードから外部のコードへと向かいます。
一方で、制御の反転 (IoC) では、この制御の流れが反転します。 フレームワークなどの外部のコードが制御の主体となり、アプリケーションコードを呼び出すようになります。 つまり、制御の流れは外部のコードからアプリケーションコードへと向かいます。 IoCを用いることで、フレームワークに多くの責務を委ねることができ、アプリケーションコードはビジネスロジックに集中することができるため、より関心の分離が促進されます。
コマンドクエリ分離の原則
コマンドクエリ分離 (Command Query Responsibility Segregation; CQRS) の原則とは、オブジェクト指向における原則で、各メソッドは以下の性質のどちらかしか持たないようにすべきである、という原則です。
- 結果を返すが、システムの観察可能な状態を変えることはない(クエリ)
- 状態を変えるが、どのような値も生成しない(コマンド)
メソッドをクエリとコマンドに分離することで、何をするメソッドなのかを理解しやすくなります。
デザインパターン
Adapterパターン
デザインパターンの一つであるAdapterパターンは、2つの異なるインターフェイスを、Adapterクラスを経由して使えるようにするための仕組みです。 コンセントの例を使うと、海外旅行したときに他国のコンセントの形状と自分の充電器のプラグの形が違うとき、変換アダプタを間に挟むことで充電することができます。
UML図でAdapterパターンの一般的な構造を表すと以下のようになります。 Adapterパターンを用いることで本来であれば対象のインターフェイスを実装していないため呼び出せないクラス (Adaptee) をそのインターフェイスを実装したクラス (Adapter) 内で呼び出すことで目的のクラス (Adaptee) を利用できるようになります。 こうすることで、インターフェイスと互換性のないクラスであっても連携させられるようになります。
Decoratorパターン
デザインパターンの一つであるDecoratorパターンは、あるインターフェイスに対して同じメソッド名のまま機能を追加するような仕組みのことです。 Decoratorパターンで実装することで、透過的なインターフェイスとして、中身を変えずに動的に機能追加をすることができます。 例えば、コンセントの例を使うと、コンセントとパソコンの間にUPSを挟むことで、インターフェイスを変えずに停電時にも稼働し続けられる機能を追加することができます。
Decoratorパターンは、依存の注入における介入 (Interception) を行うときに出てくるパターンです。
Compositeパターン
デザインパターンの一つであるCompositeパターンは、再帰的な構造を同じインターフェイスで操作できるようにするための仕組みのことです。 例えば、ディレクトリとファイルのような再帰的な構造を管理するときなどにCompositeパターンで実装することができます。 コンセントの例を使うと、複数のコンセントを持つ電源タップを使って再帰的にタコ足配線を作ることができるのは、Compositeパターンになります。
依存の注入 (Dependency Injection)
依存の注入、または依存オブジェクト注入とは、コンポーネント間の依存関係を管理するための手法です。 この手法を用いることで、コンポーネント(クラス)間の結合度を下げ、アプリケーションの柔軟性、拡張性、テスト容易性を向上させることができます。 Dependency Injectionの基本的な考え方としては、あるクラスが他のクラスのインスタンスを使用するときに、そのインスタンスを自ら作成するのではなく、外部から提供してもらうというものです。 これにより、コンポーネント間の依存関係が明確になります。 また、依存するコンポーネントを抽象化(インターフェース化)することで、実装の詳細から切り離すことができ、コードの変更が他の部分に与える影響を小さくすることができます。 Dependency Injectionには様々な形態があります。
-
コンストラクタ経由の注入 (Constructor Injection):コンストラクタの引数を経由して依存を受け取る方法。メソッド呼び出しごとに変わらない依存を提供するときに利用します。合成基点 (Composition Root) 内で適用されます。
public class MySampleClass() { public MySampleClass(IMessageWriter writer) { this.writer = writer; } }
-
メソッド経由の注入 (Method Injection):メソッドの引数を通じて依存オブジェクトを受け取る方法。メソッド呼び出しごとに異なる依存を提供できるメリットがあります。合成基点 (Composition Root) の外でのみ適用されます。
public class MySampleClass() { public WriteLog(IMessageWriter writer) { writer.WriteLog(); } }
-
プロパティ経由の注入 (Property Injection):プロパティを経由して依存を受け取る方法。デフォルトの振る舞いを持つ依存を上書きできるようにしたいとき、このパターンを利用します。このパターンはセッター経由の注入 (Setter Injection) とも呼ばれます。
public class MySampleClass() { public IMessageWriter writer { get; set; } }
依存注入で最も使われる頻度が高いのは、コンストラクタ経由の注入です。しかし、利用場面や制約によっては、メソッド経由やプロパティ経由の注入の方が適切な場面もあります。
ローカルデフォルト
ローカルデフォルト (Local Default) とは、同じモジュールや同じ層にある依存の実装クラスのことで、その実装クラスにはデフォルトと振る舞いが定義されています。 ローカルデフォルトが存在しないときは、コンストラクタ経由の注入 (Constructor Injection) を実装し、ローカルデフォルトが存在する時は、メソッド経由の注入を実装するのが推奨されます。
Nullオブジェクト
Nullオブジェクトとは、何もしない依存として実装され、依存注入によって置き換えることができるような設計パターンのことです。 振る舞いを持たない実装クラスのNullオブジェクトを注入することで、その依存を利用するクラスはnullチェックの煩わしさを抱えることなく、そのNullオブジェクトを他の依存と同じように扱うことができます。 また、場合によっては何もしないローカルデフォルトを用意して、拡張できる部分だけを提供したいときがあります。このような場合、ローカルデフォルトに対してNullオブジェクトを適用することができます。
依存注入の特性
依存注入における重要な特性には「オブジェクト合成」「介入」「生存管理」の3つがあります。
- オブジェクト合成 (Object Composition):依存のオブジェクトを生成し、その依存を必要とするクラスに注入して、そのクラスのオブジェクトを生成することです。
- 生存管理 (Lifetime Management):依存を利用するクラスがその依存の生存期間について意識しなくて良いということは、その依存を利用するクラスの責務がより簡潔になります。
- 介入 (Interception):依存の注入を外部に任せることは、対象となる依存が注入される前に、外部に対してその依存を別のものに変えることを許可することを意味します。これはデザインパターンにおけるDecoratorパターンに相当します。
依存注入の境界線
依存注入において何を注入して何を注入しないかの境界線となる要素は「安定」と「揮発性」です。 安全依存のコンポーネントは依存注入をする必要がなく、揮発性依存のコンポーネントは依存注入をする必要があります。
- 安全依存 (Stable Dependency):新しいバージョンが出ても後方互換がサポートされ、決定的な振る舞いをするクラスに依存すること。組み込みのクラスなど
- 揮発性依存 (Volatile Dependency):実行環境に対する設定や調整が必要なクラスや、乱数や現在時間などの非決定的な振る舞いをするクラスに依存すること。自作のクラスやライブラリのクラスなど
合成基点 (Composition Root)
合成基点 (Composition Root) とは、依存注入において依存の分離が完全に行われたときにオブジェクトの生成を一箇所で行う場所のことです。 合成基点はアプリケーションのエントリーポイントに限りなく近いところに配置されます。
var homeController = new HomeController(
new ProductService(
new SqlProductRepository(
new CommerceContext(connectionString)),
new AspNetUserContextAdapter()));
オブジェクト合成 (Object Composition) とは、関連するコンポーネントを用いて階層(オブジェクトグラフ)を構築する行為のことです。 オブジェクト合成は、合成基点内で実施されるようにします。
インターフェース vs 抽象クラス
Dependency Injection (注入依存) においては、抽象に「インターフェース」を用いることが推奨されています。 抽象に抽象クラスを使うことも可能ですが、以下の問題点があります。
- 抽象クラスは基底クラスとして乱用されやすい。基底クラスは永遠に変わり続ける神オブジェクトになりやすく、神オブジェクトの基底クラスを継承したクラスは、その基底と密結合してしまうため、将来的に基底クラスの振る舞いに変更が入ったときに問題となってしまいます。一方でインターフェイスを用いれば「継承よりも合成」(コードの再利用を実現するために基底クラスを継承するのではなく必要な機能性が実装された他のクラスを含めるようにする原則)の選択を強いることができます。
- 抽象クラスを実装した場合、子クラスは1つの基底クラスしか継承できない。一方でインターフェイスであれば複数のインターフェースを実装することができます。
介入
介入 (Interception) とは、連携する2つのオブジェクト間の呼び出しに介入し、その呼び出された振る舞いに対して追加の処理を加えたり、別の振る舞いに変えたりすることを、2つのオブジェクトに対して何も変更を加えることなしに行えるようにすることです。 デザインパターンの「Decoratorパターン」に相当する考え方です。 依存注入が正しく実装できていれば、Decoratorパターンを適用するのは容易なことです。
横断的関心事
横断的関心事 (Cross-cutting Concern) とは、本来の責務とは関係ない多くの異なるモジュールや異なる層にあるコードに触れないといけない事柄のことです。 横断的関心事には以下のようなものがあります。
- 監査証跡:データの変更に関するすべての処理は、その処理が行われたタイムスタンプ、実行したユーザID、変更内容を残さないといけない要件
- ログ出力:アプリケーションの状態やイベントを記録し、保守性を高めたいという要件
- パフォーマンスの監視:システムのパフォーマンスを定期的に記録しないといけない要件
- 妥当性確認:複数のレポジトリにまたがった入力値の妥当性チェックを行う要件
- セキュリティ:特定のユーザやロールでしか実行できない機能を満たす要件
- キャッシュ:使用するデータをキャッシュするかしないかをデータアクセス層のコンポーネントを利用するプログラムが選択できる機能
- 例外ハンドリング:特定の例外を適切に扱い、その情報をログに記録したり、ユーザにメッセージを表示したりする要件。Decoratorパターンを使えば例外を適切に扱えるようになる
- 耐障害性:プロセス外のリソースは常に利用できるとは限らないです。その場合、Decoratorパターンを使って障害発生時の復旧ロジックを実装することができます。マイクロサービスアーキテクチャにおけるサーキットブレイカーの設計パターンが参考になります。
横断的関心事に対しては、Decoratorパターンを用いて介入 (Interception) することで、依存注入に基づいた実装をすることができます。
テスト
テスト容易性
テスト容易性 (Testability) とは、アプリケーションの設計が単体テスト(ユニットテスト)を行いやすさを表します。 単体テストを行うことによって、アプリケーションが正しく動作するかを素早く確認できますが、それはあくまでテスト対象が依存関係にあるものから適切に隔離されている場合のみ実現できます。 そして、テスト容易性を確立するための最も安全な方法がテスト駆動開発 (Test-Driven Development) になります。
テストダブル
ソフトウェアのテストにおいて、依存となるインターフェイスの実装クラスを実際に使われるクラスではなく、その代わりとして振る舞う大体のクラスを用いて、テスト対象のオブジェクトを操作するというテクニックがあります。 このときに使われる代替クラスはテストダブル (Test Double) と呼ばれています。 ちなみに Double には影武者という意味があり、本来の依存とは異なるものをテストで使っているところに由来します。 テストダブルにはいくつかの種類があります。
- スタブ:テスト対象が依存を呼び出した時に取得できる値を用意する。
- モック:テスト対象が依存を呼び出したときに与えた値を記録する。
- スパイ:モックと同じで外部への出力を検証する目的で使用する。
- フェイク:テストを実行するための軽量化された依存(テスト時にDB接続にはインメモリのSQLiteに切り替えるなど)
接合部
接合部 (Seam) とは、アプリケーションを構築するためにアプリケーションを構成する要素の組み立てを行う場所のことです。 テストを難しくするコードを抽出し、接合部を作ることで、ユニットテストするときには別のテスト用の振る舞いをするコードに置き換えることができます。
データアクセス
DTO
DTO (Data Transfer Object) とは、クラス間のデータのやり取りのために使われるクラスで、状態のみを扱うクラスのことです。
POCO
POCO (Plain Old CRL Object) とは、.NET共通言語ランタイムのみに依存するプレーンなオブジェクトのことで、状態と振る舞いを扱うクラスのことです。 Javaの場合は POJO (Plain Old Java Object) と呼ばれています。 DTOと比較して、DBのテーブルに格納するときの値の条件(必須や制約)などの処理をつけることができます。
また、永続性無知 (Persistence Ignorant) とは、エンティティが永続化フレームワークへの依存を一切持たないPOCP (Plain Old CRL Object) のことです。
リポジトリ
リポジトリ (Repository) とはデータアクセスに関する抽象化の方法の一つで、直接DBと通信してデータの永続化と再構築を行うのではなく、インターフェースであるリポジトリを経由して行うことです。 リポジトリを経由することで、ソフトウェアに柔軟性を与えることができ、単体テストにおけるスタブやモックとしても振る舞うことができます。
エンティティ
エンティティ (Entity) とは、ドメイン駆動設計 (Domain-Driven Design; DDD) から来た用語で、特定のインスタンスに紐づかない長期にわたって生存する同一性 (Identity) を持つドメイン・オブジェクトのことです。 多くの場合、エンティティはデータベースに保持する情報を格納できるようにし、主キーをそのエンティティの同一性を表すものとして扱います。
アスペクト思考プログラミング
アスペクト思考プログラミング (Aspect-Oriented Programming; AOP) とは、横断的関心事を効果的に適用し、保守しやすいようにするにはどうすれば良いか、ということに注目したプログラミングの考え方です。 アスペクト思考プログラミングを実践するためには、SOLID原則に従う必要があります。
パラメータオブジェクト
パラメータオブジェクトまたは値オブジェクトとは、コンストラクタやメソッドの引数の中で、自然と1つにまとめることができる引数を集めて1つのオブジェクトにしたもののことです。 パラメータオブジェクトを抽出することで、様々なコンポーネントが同じ構造を持てるようになり、その結果、再利用可能な抽象を定義できるようになります。
受動的属性
受動的属性 (Passive Attributes) とは、振る舞いではなくメタデータを提供する属性のことです。 受動的属性を使うことで、関心の分離を実現することができます。 C#では、属性(Attribute)を使うことで、対象のクラスやメンバーに追加情報(例えば、このメンバーはクラス内からのみ参照可能などの情報)を与えることができます。
public class PermittedRoleAttribute : Attribute {
public readonly Role Role;
public PermittedRoleAttribute(Role role) {
this.Role = role;
}
}
public enum Role {
Administrator,
InventoryManager
}
上記の属性を使って、クラスを拡張すると以下のようになります。
[PermittedRole(Role.Administrator)]
public class AdjustInventory {
public Guid ProductId { get; set; }
public int Quantity { get; set; }
}
[PermittedRole(Role.InventoryManager)]
public class UpdateProductReviewTotals {
public Guid ProductId { get; set; }
public ProductReview[] Reviews { get; set; }
}
このように、受動的属性やパラメータオブジェクトの定義の一部として使うことができます。
アンチパターン
神オブジェクト
神オブジェクトとは、あまりにも多くのことを知りすぎたり、機能を持っていたりするようなオブジェクトのこと。 単一責任の原則に従った設計をして、コンポーネントが保持する責務を小さくする必要がある。
一時的結合
一時的結合 (Temporal Coupling) とは、コードの嫌な臭い (Code Smell) の1つで、クラスの中にある異なるフィールドやメソッドなどが暗黙的に結びついてしまう状態のこと。 例えば、メソッドAを呼ぶ前に必ずメソッドBを呼ばないといけないような状態で、コンパイルは成功するが実行時にエラーする可能性があるプログラムのこと。 メソッド経由の依存注入において、引数で与えられた依存をプライベートなフィールドに保存して使い回すようなことを行わないように実装を修正する必要があります。
var sample = new Sample();
sample.Initialize(new Config()); // アンチパターン:fetch()を呼ぶ前に初期化が必要
sample.url = new Url("http://example.com"); // アンチパターン:fetch()を呼ぶ前にurlプロパティの初期化が必要
sample.Fetch();
コントロールフリーク
コントロールフリーク (Control Freak) とは、制御の反転 (Inversion of Control) とは逆に、依存を直接制御するアンチパターンのことです。 この問題点は、密結合したプログラムをが作成され、テスト容易性やモジュールの再利用性が損なわれてしまうことです。
public class MySample {
public MySample() {
this.repository = new SqlProductRepository(); // アンチパターン!
}
}
ローカルデフォルトとしてデフォルトをインスタンス化することは問題ないのですが、外部デフォルトでは問題になります。 外部デフォルト (Foreign Default) とは、ローカルデフォルトとは反対のもので、依存がそのクラスとは別のモジュールに定義されているにもかかわらず、デフォルトの依存として使用されている揮発性依存のことです。
サービスロケータ
サービスロケータ (Service Locator) とは、合成基点 (Composition Root) の外でアプリケーションを構成するオブジェクトに揮発性依存の提供を無制限に行えるようにするアンチパターンのことです。 よくある実装としては静的ファクトリー(静的クラスのメソッドでコンストラクタを返す実装)があります。
public class MySample {
public void FetchData() {
IProductService service = Locator.GetService<IProductService>(); // アンチパターン!
var products = service.GetProducts();
}
}
サービスロケータを利用したときの問題点は、サービスロケータを利用するクラスは内部で利用している依存が何かを外部から隠してしまう点です。 コンストラクタやメソッドの引数の型から、何の依存を注入しているのかが把握できなくなってしまいます。 また、サービスロケータを用いていることで発生した実行時エラーは、原因特定までに時間がかかります。 さらに、単体テストにおいても、テストを並列で実行した時に、サービスロケータが他のコードに影響を与えてしまう問題もあります。
アンビエントコンテキスト
アンビエントコンテキスト (Ambient Context) とは、静的なメソッドやプロパティを利用することで合成基点 (Composition Root) の外で揮発性依存へのグローバルなアクセスや振る舞いをアプリケーションコードに提供するアンチパターンのことです。 構造的にはシングルトン (Singleton) パターンに似ていますが、シングルトンはその単一のインスタンスが決して変わらないのに対して、アンビエントコンテキストはそのインスタンス(依存)が変更できる点があり、それが単体テストの並行実行時などに問題になります。
public class MySample {
public void GetTime() {
ITimeProvider provider = TimeProvider.Current; // アンチパターン!:静的クラスのプロパティを経由してインスタンスを利用している
DateTime now = provider.Now();
}
}
制約に縛られた生成
制約に縛られた生成 (Constrained Construction) とは、遅延バインディングを実現するために、特定の抽象に対するすべての実装クラスが全く同じコンストラクタを持つことを強制するアンチパターンのことです。
// 設定ファイルから型名を取得する
string productRepositoryTypeName = settings.GetValue<string>("ProductRepositoryType");
// 型を取得し、動的にインスタンス化する
var productRepositoryType = Type.GetType(productRepositoryTypeName);
var constructorArguments = new object[] { connectionString };
IProductRepository repository =
(IProductRepository)Activator.CreateInstance(productRepositoryType, constructorArguments) // アンチパターン!
遅延バインディング (Late Binding) とは、コードを再コンパイルすることなくアプリケーションの一部の機能を取り替えられるための仕組みのことです。 ただし、遅延バインディングを動的な型生成で実現してしまうと、依存のコンストラクタに対して意図しない制約(上記の場合はconnectionStringという引数)を強制してしまいます。 この場合のリファクタリングの方法としては、抽象ファクトリ (Abstract Factory) があります。
var factory = Factory.GetFactory(productRepositoryTypeName);
IProductRepository repository = factory.CreateRepository(productRepositoryType, constructorArguments);
ただし、抽象ファクトリの過度な利用は、コードが複雑になる原因になります。
コードの嫌な臭い
コードの嫌な臭い (Code Smell) とは何か間違っている気はしても、その間違いを明確に指摘できないコードのことを意味します。 アンチパターンは必ず問題を起こすために常に避けるべきですが、一方でコードの嫌な臭いは問題を起こす可能性がありそうなコードのことで、必ずしもすぐに問題に直結するわけではありません。
コンストラクタ経由での過度な注入
依存注入においてコンストラクタ経由での注入は最も使われる方法ですが、コンストラクタ経由での過度な注入 (Constructor Over-Injection) が発生している場合は、単一責任の原則を満たしていない可能性があるため、コードを見直す必要があります。 また、この問題をリファクタリングするときに用いられる方法には主に以下の2つがあります。
(解決策1)Facadeサービス
Facade(ファサード)は、デザインパターンの1つで、複数のコンポーネントを1つにまとめて提供する仕組みです。 Facadeはフランス語で建物の正面という意味です。 まず、Facadeパターンを使わずに実装すると、以下の例では4つの依存をコンストラクタ経由で注入する必要があります。
一方で、Facadeパターンを利用して実装すると、以下の例ではコンストラクタ経由で注入する必要がある依存が3つに減らすことができます。
Facadeサービスでリファクタリングすることは、ただ依存の数を減らすためだけではなく、発生するコンポーネント間のやり取りにおいて自然と形成される依存の集まりを識別できるようになることが重要になります。
(解決策2)ドメインイベント
ドメインイベント (Domain Events) とは、アプリケーションの状態を変えることへの引き金となるアクションを捉えるためのモデルです。
例えば、「タスクが登録された」といったものがイベントです。
これに TaskRegistered
のような名前をつけてクラスとして扱う手法がドメインイベントパターンです。
まず、イベントを処理するインターフェイスを作成し、そのインターフェイスを実装したクラスを実装していきます。
public interface IEventHandler<TEvent> {
void Handle(TEvent e);
}
そして、合成基点にて複数のイベントハンドラを扱うクラスを Composite パターンで実装し、次のようなイベントハンドラークラスをインスタンス化します。
var orderApprovedHandler = new CompositeEventHandler<OrderApproved>(
new IEventHandler<OrderApproved>[] {
new OrderApprovedReceiptSender(messageService),
new AccountingNotifier(billingSystem),
new OrderFulfillment(locationService, inventoryManagement),
}
);
var orderService = new OrderService(orderRepository, orderApprovedHandler);
このように、Composite パターンで複数のイベントハンドラを含めた依存を、コンストラクタに渡すことで、必要な依存の見通しが良くなります。
循環依存
循環依存が発生した場合、設計に問題があるため、すぐにその根本となる原因を徹底的に調査すべきです。 循環依存に対してよく用いられる戦略には以下のものがあります。
(解決策1)クラスの分割
ほとんどの場合、非常に多くのメソッドを持つクラスをより細かなクラスに分割することで循環依存は解決されます。
(解決策2)イベント駆動
依存に対して何かを行わせるときに、その依存を直接呼び出す代わりに、イベントを発火させ、そのイベントリスナーを起動させることで循環依存が解消されます。ただし、対象のメソッドは戻り値がない場合のみ使える戦略です。 イベント駆動は、デザインパターンのオブザーバ (Observer) パターンやPublish/Subscribe (Pub/Sub) パターンとも呼ばれます。