Kōhei Yamamoto

『なぜ依存を注入するのか DIの原理・原則とパターン』を読んだ

『なぜ依存を注入するのか DIの原理・原則とパターン』を読んだ。

なぜ依存を注入するのか DIの原理・原則とパターン (Compass Booksシリーズ) | Steven van Deursen, Mark Seemann, 須田智之 |本 | 通販 | Amazon

内容

依存性の注入 (dependency injection; DI) について、主に静的型付け言語からの観点で網羅的に解説している本。

構成としては、まずDIの原理について説明し、次にパターンとアンチパターンのカタログ。その後、ライブラリを使わないDIであるPure DIの実装方法について詳細に説明する。最後に、ライブラリとして提供されているDIコンテナをなぜ、どう使うのかを解説する。

DIが必要な理由を「モジュールの疎結合化を手段とする保守性の向上」という観点で説明し、アプリケーションのエントリポイントから呼び出されるモジュール群のうち、密に結合すると保守性の観点で問題が起きうる依存(揮発性依存)に限って、DIで注入すべきと主張をしている。

ここで、ある依存が揮発性依存であることの必要条件として、環境に対する設定を要する別プロセスで動くサーバ(DBやメールなど)のためのモジュール、また乱数や時刻など非決定的な振る舞いを含むモジュールを例に挙げている。逆に、安定依存であることの必要条件としては、言語付属のライブラリや、アプリケーションの対象ドメインに深く関連するモジュールを挙げている。

サンプルコードはC#と.NETを前提としている。また、DIコンテナのセクションもAutofac、Simple Injector、MS.DIをもとに解説を展開している。

どうでもいい点として、料理の例えが多い本だが、個人的には一貫してピンとこなかった…。

以降、いくつか勉強になった箇所を挙げておく。

Pure DIとコンストラクタ経由での注入

「DI = DIコンテナを使うこと」という図式になりやすいが、この本ではDIのためのライブラリを使わない「Pure DI」をまず説明している。

Pure DIの実装例として、Webアプリケーションのエントリポイントで、次のようにHomeControllerを起点としたオブジェクトグラフを組み上げる箇所(合成基点)を設けるコードが示されていた。

p.93 リスト3.13から抜粋
new HomeController(
new ProductService(
new SqlProductRepository(
new CommerceContext(connectionString)),
new AspNetUserContextAdapter()));

Pure DIの利点は、言語の機能に関する知識だけでDIを実現できる点。アプリケーションの規模が小さければ、DIコンテナのようなライブラリを使う場合に比べて、認知負荷を軽減できる可能性がある。

また、依存を注入する経路として、コンストラクタ、メソッド、プロパティ(セッター)のどれを選ぶべきかの判断があるとしている。上記サンプルコードのようにコンストラクタ経由での注入を基本とし、必要に応じてメソッド経由での注入を選ぶという基準が示されていた。

アンチパターンとしてのサービスロケータ

DIのアンチパターンがいくつか解説されている。そのなかでもサービスロケータはまさにアンチパターン(よかれと思ってやってしまうが、悪い結果をもたらすパターン)の様相を呈している。

サービスロケータは、合成基点以外の場所で揮発性依存を取得できる仕組みを作ってしまうことだといえる。次のサンプルコードだと、本来は依存が注入されるべきHomeControllerの内部で、外からわからない形でLocator.GetServiceによって揮発性依存を取得している。

p.169 リスト5.5から抜粋
public class HomeController : Controller
{
public HomeController() {}
public ViewResult Index()
{
IProductService service =
Locator.GetService<IProductService>();
var products = service.GetFeaturedProducts();
return this.View(products);
}
}

このパターンの主な欠点は、サービスロケータに依存しているかどうかがインタフェースに現れないことだ。上記サンプルコードだとHomeController.Indexのシグネチャを見ても、IProductServiceに依存していることがわからない。つまり、DIを使わないときの欠点が現れてしまっている。この場合はコンストラクタ経由で依存注入すべき。

アンチパターンとしてのサービスロケータが発生しやすい理由には、本質的にDIコンテナと同じ仕組みであることが大きい。両者は、あらかじめインタフェースに関連付けた揮発性依存を取得するAPIを提供するという点で共通している。しかし、DIコンテナは合成基点だけで用いることを意図しているのに対して、サービスロケータはそうではない。

AOP

SOLID原則とDIを活用することで、特定のツールを導入せずとも、設計によってアスペクト指向プログラミング (AOP) が実現できる。ここで、この本ではAOPを「横断的関心事や同質の設計パターンを……一箇所で記述し、その記述したものをコードベースに宣言的もしくは規約をもとに適用する」方法(p.374より)と定義している。

適切な粒度の抽象インタフェースを設計し、その抽象に基づく具象クラスのロジックにDecoratorパターンで介入することで、特定のツールに頼らずともPure DIの形でAOPを実現できる。たとえば、次のサンプルコードでは、AdjustInventoryServiceのロジックに対して介入している。監査証跡、DBトランザクション、セキュリティという3つの横断的関心事について、デコレータで包んで実行できるオブジェクトをDIによって作り出している1

p.413 リスト10.20から抜粋
ICommandService<AdjustInventory> service =
new SecureCommandServiceDecorator<AdjustInventory>(
this.userContext,
new TransactionCommandServiceDecorator<AdjustInventory>(
new AuditingCommandServiceDecorator<AdjustInventory>(
this.userContext,
this.timeProvider,
context,
new AdjustInventoryService(repository))));
return new InventoryController(service);

感想

あくまでも静的型付け言語からの観点ではあるが、DIの原理の理解、つまりインタフェースに対するプログラミングと合成基点での依存注入によるオブジェクト合成について、また、DIが必要な場面だと判断するための基準、具体的なパターンや実装方法を網羅的に確認できた。以前、DIを学ぶならという話がfukabori.fmであった2が、和書でDIについて学びたい場合の書籍はこの本でしばらく問題なさそうだった。

まず考慮すべき選択肢としてPure DIが提示されているが、デファクトスタンダードになっているDIコンテナがある環境3なら、デフォルトでDIコンテナを使うほうが開発時の認知負荷が下がることはありえそうだと思った。そうでない環境なら、Pure DIをまず選択し、そこからアプリケーションがスケールするに従って、本書でも紹介されている基準でDIコンテナを使うかどうかの判断が求められると感じた。

C#と.NETを用いた例が示される本ではあるが、それ自体はある意味で実装の詳細といえる箇所も多い4。個々の技術の使い方ではなく根底にある考え方を学ぶのがよいと思った。サンプルコード自体は『脳に収まるコードの書き方』のMark Seemann氏が共著に入っていることもあってか、読みやすいものだった。

あくまでも静的型付け言語や関連するフレームワークを前提とした本だが、動的型付け言語やopinionatedな環境でも活かせる考えかたはあるとも思った。たとえば、Railsはあえて密結合なアーキテクチャを選んでいるとよく言われる。これを安定依存・揮発性依存の枠組みで言い直すと、Railsがサポートする技術領域(Active RecordでRDB、Action Mailerでメール、など)は安定依存である、という割り切りがあるということだと思う。なので、フレームワークのサポートのもと、ユニットテストでも常にテスト対象になる。

一方、Railsがサポートしない技術領域、たとえば決済やLLMなどは揮発性依存と考えて、依存注入できるようにしておくと、この本で説明されているような利点が得られそうだ。個人的には、RDBの変更よりは、決済代行システムや生成AIサービスのほうが、さまざまなサービスを同時に使用したり乗り換えたりする可能性が高そうな感覚がある。また、それらの依存を注入できる設計にすると、(細かい話だが)テストでRSpecのallow_any_instance_ofを回避してテストダブルを注入できるなどのテスティングにおける利点もある。

この本では、DIの実装において、言語機能としてのインタフェースで抽象を定義する方法が用いられている。しかし、動的型付け言語だとそうはいかないので、なんらかのケアが必要になる。具体的にはダックタイピングを通じてインタフェースを見出し、(Rubyなら)モジュールやテストを用いてインタフェースを維持することになる。このあたりの話は『オブジェクト指向設計実践ガイド』が参考になる。

脚注

  1. AOPの定義に含まれている「宣言的」な方法については、C#の属性を用いたメソッドのアノテーションが書籍内で解説されているが、ここでは省略した

  2. 「話したネタ」に原著のAmazon商品ページへのリンクが貼ってあった

  3. たとえばLaravelならフレームワークにDIコンテナが備わっている (https://laravel.com/docs/12.x/container)

  4. ただし、最後の13–15章については、DIコンテナライブラリを比較するために、それらの使いかた自体にフォーカスしている