依存関係の注入は引数を渡すこと
2025-06-17 10:00:25
原文: Dependency injection is passing an argument by Mark Seemann
依存関係の注入とは、本当に単なる引数渡しなのでしょうか?本稿で簡潔に見ていきましょう。
この記事は、依存関係の注入から依存関係の排除へという短い連載記事の最初の記事です。
2012年のNortheast Scala Symposiumでの講演で、Rúnar Bjarnasonはさりげなくこう述べました。依存関係の注入とは「『引数を取る』ということをもったいぶって言っているだけだ」と。私は500ページを超える依存関係の注入に関する本を執筆した人間なので、あなたはこの意見に私が異を唱えるだろうと思われるかもしれません。しかし、その言葉にはある程度の真実が含まれています。もっとも、事はそれほど単純ではありませんが。
この記事では、いくつかの簡単な例を挙げ、Rúnar Bjarnasonが正しいと言える点と、一方で、それだけでは終わらない少し複雑な事情がある点を説明します。
レストラン予約の例
この連載の他の記事と同様に、オンラインのレストラン予約をサンプルシナリオとして用います。あなたは、レストランの予約情報を含むJSONドキュメントを受け付ける、HTTPベースのAPIを開発するよう依頼されたと想像してみてください。さらに、その開発にはC#とASP.NET Web APIを使い、ドメイン駆動設計の採用を目指していると仮定します。
飛んでくるPOSTリクエストを処理するために、次のようなアクションメソッドを書くことになるでしょう。
public IHttpActionResult Post(ReservationRequestDto dto)
{
var validationMsg = validator.Validate(dto);
if (validationMsg != "")
return this.BadRequest(validationMsg);
var r = mapper.Map(dto);
var id = maîtreD.TryAccept(r);
if (id == null)
return this.StatusCode(HttpStatusCode.Forbidden);
return this.Ok();
}
このメソッドは、入力の検証、ドメインモデルへのマッピング、そのモデルへの処理の委譲、処理後の状態の確認、そして結果の返却という、シンプルでおなじみの手順を踏んでいます。
しかし、このメソッドがすべての作業を自分自身で行っているわけではないことにお気づきかもしれません。validator、mapper、MaitreDといった協力オブジェクトに作業の一部を委譲しています。これらの協力オブジェクトはどこから来るのでしょうか?
それらは依存関係です。Postメソッドに、これらを引数として渡すことはできるでしょうか?
残念ながら、それはできません。PostメソッドはHTTP APIの境界の一部を構成しているからです。ASP.NET Web APIは規約に基づいてHTTPリクエストのルーティングとディスパッチを行い、アクションメソッドはその規約に従わなければなりません。好きな引数を自由に関数に渡せるわけではないのです。そのため、これらの依存関係をオブジェクトに渡すための別の場所を見つける必要があります。
(Postメソッド自体に渡すのが最善だとした場合の)次善の策は、コンストラクタ経由で渡すことです。
public ReservationsController(
IValidator validator,
IMapper mapper,
IMaîtreD maîtreD)
{
this.validator = validator;
this.mapper = mapper;
this.maîtreD = maîtreD;
}
これはコンストラクタインジェクションと呼ばれるデザインパターンの適用例です。これにより、依存関係がクラスのフィールドに保持され、クラスのメンバー(Postメソッドなど)から利用できるようになります。
これは、決まって現れるパターンなのです。
依存関係はどこまでも続く
Postメソッドはシステムの境界に位置し、特定のルールに従わなければならないため、特殊なケースだと主張することもできるでしょう。一方で、実装のより深い階層ではこれらのルールは適用されません。では、他のオブジェクトは、依存関係を単純に引数として渡すだけで実装できるのでしょうか?
例として、IMaitreD.TryAcceptの実装を考えてみましょう。
public int? TryAccept(Reservation reservation)
{
var reservedSeats = reservationsRepository
.ReadReservations(reservation.Date)
.Sum(r => r.Quantity);
if (reservedSeats + reservation.Quantity <= capacity)
{
reservation.IsAccepted = true;
return reservationsRepository.Create(reservation);
}
return null;
}
このメソッドにはreservationsRepositoryという、もう一つの協力オブジェクトがあります。これもまた依存関係です。これはどこから来るのでしょうか?
TryAcceptメソッドにreservationsRepositoryを引数として渡すことはできるでしょうか?
残念ながら、それも不可能です。なぜなら、このメソッドはIMaitreDインターフェースによって定義されているからです。
public interface IMaîtreD
{
int? TryAccept(Reservation reservation);
}
前述のPostメソッドが、具象クラスではなくIMaitreDインターフェースに対してプログラミングされていたことを思い出してください。IMaitreD.TryAcceptにIReservationsRepositoryを引数として追加するのは、漏れのある抽象になってしまいます。なぜなら、このインターフェースのすべての実装がその依存関係を必要とするとは限らないからです。あるいは、別の実装がまた別の依存関係を持つかもしれません。その依存関係もIMaitreD.TryAcceptの引数リストに追加すべきでしょうか?
それは、到底擁護できる設計原則ではありません。一方で、コンストラクタインジェクションを使えば、実装の詳細を抽象から切り離すことができます。
public MaîtreD(int capacity, IReservationsRepository reservationsRepository)
{
this.capacity = capacity;
this.reservationsRepository = reservationsRepository;
}
このコンストラクタは、IReservationsRepositoryオブジェクトだけでなく、対象となるレストランの収容人数を表す整数も受け取ります。これは、依存関係がプリミティブ値になることもあるということを示しています。
まとめ
ある意味で、依存関係の注入とはオブジェクトが引数を取るための特定の方法にすぎません。しかし、多くの場合、オブジェクトは自身が実装するインターフェースによって定義された役割を持っています。そうしたオブジェクトは、インターフェースで定義されたAPI経由では利用できない協力オブジェクトを必要とする場合があります。そのため、対象の具象クラスに属するメンバーを介して依存関係を供給する必要が出てきます。クラスのコンストラクタ経由で依存関係を渡すことが、そのための最善の方法です。
次へ: 部分適用は依存関係の注入である