ダブルディスパッチC#:依存性の注入が意味を持つとき - Derek Comartinのドメイン駆動設計の例を通して説明する
C#プログラミング言語の分野では、ダブルディスパッチが誤解されたり、十分に活用されていないことがよくあります。 これは、関係する2つのオブジェクト間でポリモーフィックな振る舞いを可能にする強力なテクニックで、特に派生クラス間の振る舞いを処理する場合に有効です。
Double Dispatch in DDD: When Injecting Dependencies Makes Sense"というビデオで、Derek Comartin は、ダブルディスパッチがドメイン駆動設計(DDD)にどのように完璧に適合するかを説明しています。 メンテナンスの負担を軽減し、既存のコードを簡素化し、さらには他の言語でよく見られるビジター・パターンのようなパターンを模倣する方法を示しながら、彼の例を深く掘り下げていきます。
C#のシングルディスパッチの制限に対処したことがある場合、またはオブジェクトのインスタンスと注入されたストラテジーやルールの両方に基づいて動作を決定しようとしたことがある場合、この記事はダブルディスパッチC#がどのように効果的に使用できるかを明確にするのに役立ちます。
なぜドメイン モデルでは振る舞いを注入するのが理にかなっているのか
Derekは、DDDにおける共通の教義を指摘することから始める。 しかし、それは常に実用的で役に立つとは限りません。 動作をモデリングする際、ルール、ポリシー、ストラテジーなどのロジックを注入する必要があるかもしれません。 ポリシーのようなドメインコンセプトをドメインオブジェクトに渡し、メソッドベースの評価を外部で処理させることができます。
インフラストラクチャではなく、ドメインの動作です。
悪い例:ドメイン内のハードコードされたロジック
問題を実証するために、Derekは遅刻を判断するハードコードされたロジックを含むShipmentクラスから始めます:
public bool IsLate(DateTime expectedDelivery)
{
return _systemClock.Now() > expectedDelivery;
}public bool IsLate(DateTime expectedDelivery)
{
return _systemClock.Now() > expectedDelivery;
}この既存のコードはシンプルですが、柔軟性に欠けます。 コンパイル時の判断に依存し、動作をクラスと緊密に結合します。 ルールを変更するにはドメインモデルを変更する必要があり、テストは時間に依存するため難しくなります。
リファクター:ポリシーとダブルディスパッチを使用する
Derekは、IDeliveryTimingPolicyインターフェイスを紹介します:
public interface IDeliveryTimingPolicy
{
bool IsLate(Shipment shipment);
}public interface IDeliveryTimingPolicy
{
bool IsLate(Shipment shipment);
}その後、彼は2つの実装を作成します:
1.標準配信タイミングポリシー
2.バッファードデリバリータイミングポリシー
これらのクラスは、Shipmentを取り込み、そのルールに基づいてブール値を返します。 以下は、BufferedDeliveryTimingPolicyのコード・スニペットです:
public bool IsLate(Shipment shipment)
{
return _clock.Now() > shipment.DeliveryDate.AddMinutes(30);
}public bool IsLate(Shipment shipment)
{
return _clock.Now() > shipment.DeliveryDate.AddMinutes(30);
}さて、Shipment クラスでは、ダブルディスパッチを使用します:
public bool IsLate(IDeliveryTimingPolicy policy)
{
return policy.IsLate(this);
}public bool IsLate(IDeliveryTimingPolicy policy)
{
return policy.IsLate(this);
}これは古典的な二重ディスパッチです。最初のディスパッチは、出荷に対して IsLate() を呼び出し、ポリシーを渡します; 2番目のディスパッチは、出荷をパラメータとしてポリシー上で IsLate() を呼び出すことです。 2つのオブジェクトが関与しており、それぞれが動作の決定に関与しています。
テストにおける利点と柔軟性
このアプローチにより、テスト可能で決定性の高いコードが得られます。 Derek は、テストデータが制御され、各オブジェクトのランタイム・タイプが動作を決定する、標準ポリシーとバッファード・ポリシーを使用した例を示します。
var policy = new BufferedDeliveryTimingPolicy(TimeSpan.FromMinutes(30));
var shipment = new Shipment { DeliveryDate = DateTime.UtcNow.AddMinutes(-15) };
Assert.False(shipment.IsLate(policy)); // Because of 30-minute buffervar policy = new BufferedDeliveryTimingPolicy(TimeSpan.FromMinutes(30));
var shipment = new Shipment { DeliveryDate = DateTime.UtcNow.AddMinutes(-15) };
Assert.False(shipment.IsLate(policy)); // Because of 30-minute bufferこれは、ドメインモデルを変更することなく、実行時の動作をどのように変更できるかを示すものです。 ドメインの完全性を維持しながら、ポリシーの動的な性質に基づいたポリモーフィックな動作を得ることができます。
C#におけるダブルディスパッチ vs ビジターパターン
デレクの例は、オブジェクトの構造を変更せずに、オブジェクトに対して実行する一連の操作を定義するビジターパターンに似ています。 通常、訪問者のパターンでは、次のようなものがあります:
public abstract class Shape
{
public abstract void Accept(IVisitor visitor);
}
public class Circle : Shape
{
public override void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}public abstract class Shape
{
public abstract void Accept(IVisitor visitor);
}
public class Circle : Shape
{
public override void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}ここでは、Accept() と Visit() メソッドは、デレクの Shipment と IDeliveryTimingPolicy の使い方と同じように、2つの型(オブジェクトと訪問者)にまたがって調整されています。 このパターンは、C#のような単一のディスパッチ言語であっても、複数のディスパッチ動作を実装するのに役立ちます。
ポリシーのコレクションでルールを構成する
次の別の例では、Derekは、複数のルールがコレクションを介して評価される方法を示しています。 などのルールを紹介している:
有効な宛先ルールがある
全パッケージパックルール
- 未出荷ルール
それぞれがIShipmentReadinessRuleを実装しています:
public bool IsSatisfiedBy(Shipment shipment)public bool IsSatisfiedBy(Shipment shipment)そして、Shipmentクラスはそれらを次のように評価します:
public bool CanShip(IEnumerable<IShipmentReadinessRule> rules)
{
return rules.All(rule => rule.IsSatisfiedBy(this));
}public bool CanShip(IEnumerable<IShipmentReadinessRule> rules)
{
return rules.All(rule => rule.IsSatisfiedBy(this));
}このvoid acceptパターンは、複数のドメインルールを動的に評価することができます。 ルールをコンフィギュレーションに格納する場合(特にマルチテナント型アプリの場合)、実行時に新しいリストを作成し、それを出荷に渡すことができます。
マルチテナント アプリケーションにおける動的ルール
デレク氏は、マルチテナントのアプリケーションでは、顧客ごとにルールが変わる可能性があると指摘する。 ポリシーのリストをストレージから取得し、動的に注入することができます:
var rules = LoadRulesForCustomer(customerId);
var canShip = shipment.CanShip(rules);var rules = LoadRulesForCustomer(customerId);
var canShip = shipment.CanShip(rules);これは、動的ディスパッチと実行時の意思決定をアプリケーションにどのように組み込むことができるかを示しています。 望ましい動作は、モデルではなく、設定を変更することで実現されます。
誤解を解く:すべての依存関係が悪いわけではありません
最後にデレクは、ドメインに何かを注入することは悪いことだという誤解を取り上げる。 彼は、重要なのは何を注入するかだと強調しています。 仕様やポリシーのようなドメインの動作を注入することは、インフラストラクチャを注入することとは異なります。
ドメインモデルは、引き続きエントリポイントです。 このオブジェクトは決定を所有しますが、同じドメインコンテキスト内にある限り、他のオブジェクトに委任することができます。
まとめ:DDD において C# のダブルディスパッチが強力な理由
デレクは最後に、批判的に考えるよう促している。void visitやpublic virtual void accept、あるいは同様のパターンが明快さと保守性をもたらすのであれば、恐れることはない。 ビジネスロジックを制御された方法で注入することで、柔軟性と精度が向上します。
新しいクラスを開発する場合でも、既存のコードベースをリファクタリングする場合でも、ダブルディスパッチC#を使用すると、ドメインに焦点を当てながら、懸念事項を明確に分離することができます。
もしあなたがポリシーや仕様を使用していたり、あるいは気づかずにビジターパターンを構築しているのであれば、あなたはすでにダブルディスパッチの実装に近づいています。 これを理解することで、実行時にコードがどのように動作するかをよりコントロールできるようになり、テスト容易性と適応性が向上します。
結論
要約すると、C#のダブルディスパッチは、ドメインロジックを注入し、カプセル化を維持し、柔軟な動作をサポートするためのエレガントで実用的なソリューションになります。 visitor、multiple dispatch、dynamic keyword usage(注意深く)などのパターンと併用することで、表現力豊かで堅牢なドメインモデルを書くことができます。
次にshipment.IsLate(policy)を呼び出すときは、C#を真のポリモーフィック設計に近づける基本的なパターンを活用していることを知っておいてください。
ヒント: PurchaseOrderクラスを作成していて、ポリシーに基づいてItemを追加できるかどうかを判断したい場合、PurchaseOrderにポリシーを渡してみてください。 これがダブルディスパッチです。

