依存性逆転の原則(Dependency Inversion Principle、DIP)は、業務ルールを書いた上位のコードが、データベースやメール送信といった下位の実装を直接参照しないようにする設計原則です。SOLID原則の「D」にあたります。名前の似たDI(依存性の注入)とは別物で、DIは依存オブジェクトを外から渡す手法、DIPは依存の向きを決める原則です。この記事では、Robert C. Martinの原典に書かれた定義から始め、何が「逆転」するのか、Javaの違反コードをどう直すか、違反をテストで検出する方法、守らなくてよい場面までを順に扱います。
まとめ:依存性逆転の原則の要点とDIとの違い
- 定義:上位モジュールは下位モジュールに依存せず、両者とも抽象に依存する(原則A)。抽象は詳細に依存せず、詳細が抽象に依存する(原則B)。1996年にRobert C. MartinがC++ Reportのコラムで示した。
- 逆転するもの:依存の矢印の向き。インターフェースを上位が所有する側(上位のパッケージ、または上位のための独立した契約パッケージ)に置き、下位の実装がそれを実装する。インターフェースを下位の実装と同じパッケージに置くと、上位に下位への依存が残る。
- DIとの違い:DIは依存の渡し方(実装手法)、DIPは依存の向き(設計原則)。具象クラスをコンストラクタで渡すとDIだがDIPは満たさない。
- 守り方の確認:ArchUnit(Java)などで「上位パッケージは下位パッケージに依存しない」をテストにすると、レビューに頼らず違反を検出できる。
- 適用範囲:差し替えやテストでの置き換えが必要な境界(DB・外部API・通知など)に絞る。実装が1つで変わらない依存まで抽象化しない。
依存性逆転の原則(DIP)の定義|Martinの原則AとB
DIPの初出は、Robert C. MartinがC++ Reportに連載していたコラム「Engineering Notebook」の3回目「The Dependency Inversion Principle」(1996年)です。冒頭で前回(1996年3月号)のリスコフの置換原則に触れ、その続きとして書かれています。原典の定義は次の2文です。
- A. HIGH LEVEL MODULES SHOULD NOT DEPEND UPON LOW LEVEL MODULES. BOTH SHOULD DEPEND UPON ABSTRACTIONS.(上位モジュールは下位モジュールに依存してはならない。どちらも抽象に依存すべきである)
- B. ABSTRACTIONS SHOULD NOT DEPEND UPON DETAILS. DETAILS SHOULD DEPEND UPON ABSTRACTIONS.(抽象は詳細に依存してはならない。詳細が抽象に依存すべきである)
上位モジュール・下位モジュール・抽象の役割
上位モジュールは、アプリケーションの方針(ポリシー)や業務ルールを持つ部分です。原典は「重要な方針の決定とビジネスモデルを持つのは上位モジュールだ」と書き、その上位が下位の変更に引きずられる構造を「absurd(ばかげている)」と批判しています。下位モジュールは、DBアクセス・ファイル入出力・外部API呼び出しなど、方針を実現する手段です。抽象は、Javaならインターフェースや抽象クラス、PythonならProtocolやABCのように、振る舞いの約束だけを定めた型を指します。
原典の例は、キーボードから読んだ文字をプリンタへ書く「Copy」プログラムです。Copyが「Read Keyboard」「Write Printer」を直接呼んでいると、出力先にディスクを足すだけでCopy自体に分岐を書き足すことになります。Copyが抽象的な「Reader」「Writer」だけを知る形にすれば、入出力の種類が増えてもCopyは変わりません。
表記の揺れ:依存性逆転・依存関係逆転・依存性の逆転
Dependency Inversion Principleの訳語は定まっておらず、「依存性逆転の原則」「依存関係逆転の原則」「依存性の逆転」「依存性逆転の法則」はすべて同じ原則を指します。Dependencyを「依存性」と訳すか「依存関係」と訳すかの違いで、内容に差はありません。英語の略称はDIPで、Webの検索で見かける「DIP関節」(指の遠位指節間関節)とは無関係です。
何が「逆転」するのか|依存の矢印と抽象の置き場所
Martinが「inversion」という語を使った理由は、構造化分析・設計のような従来の手法では、上位モジュールが下位モジュールを呼び出す階層をそのまま依存の階層として作るためです。オブジェクト指向でうまく設計されたプログラムの依存構造は、それに対して「逆転」している、というのが原典の説明です。
従来の依存:上位から下位へ一直線
原典は「よく構造化されたオブジェクト指向のアーキテクチャは明確な層を持つ」というBoochの言葉を引いたうえで、Policy層→Mechanism層→Utility層と素直に積んだ構造を例に、「依存は推移する(Dependency is transitive)」と指摘しています。Policy層はMechanism層を通じてUtility層にも間接的に依存するため、最下層の変更が最上層まで波及します。業務ルールのクラスがMySQL用のリポジトリを直接newしている状態がこれにあたり、DB製品を替えるだけで業務ルールの再コンパイルとテストが必要になります。
逆転後の依存方向と抽象の配置
逆転の要点は、インターフェースを作ることそのものより、インターフェースをどちらのパッケージに置くかです。上位モジュールが必要とする振る舞い(たとえば「注文を保存する」)を上位側のパッケージでインターフェースとして宣言し、下位の実装がそれをimplementsします。すると、コンパイル時の依存は「下位→上位のインターフェース」になり、矢印が反対を向きます。Microsoftのアーキテクチャ原則の解説も、下位のBが「Aが管理するインターフェース」にコンパイル時に依存する形を、依存の逆転として図示しています。抽象を実装と別の独立したパッケージに置く構成(FowlerのSeparated Interface)でも、その抽象が上位の必要から決まり、下位の実装パッケージに含まれていなければ同じ効果が得られます。
逆に、OrderRepositoryインターフェースを下位のinfraパッケージに実装と並べて置くと、上位のorderパッケージは依然としてinfraをimportします。インターフェースは使っていても、パッケージ間の依存は上位→下位のままです。この違いは次の章のコードと、後半のArchUnitの実行結果で確かめられます。
DIPに違反したJavaコードを直す手順
注文を受け付けるOrderService(上位)が、MySQLに保存するMySqlOrderRepository(下位)を使う例で、違反の状態から直します。コードはJava 25(Temurin 25.0.4.1)でコンパイル・実行を確認しています。保存処理はSQL文字列を標準出力するだけの説明用実装で、MySQLへの接続や保存は行いません。
DIP違反例:上位による下位の具象クラスの生成
// com/example/order/OrderService.java(上位)
package com.example.order;
import com.example.infra.MySqlOrderRepository;
public class OrderService {
private final MySqlOrderRepository repository = new MySqlOrderRepository();
public void place(String orderId) {
repository.save(orderId);
}
}
orderパッケージがinfraパッケージの具象クラスをimportし、生成まで自分で行っています。保存先をPostgreSQLやテスト用のメモリ実装に替えるには、OrderServiceを書き換えるしかありません。
手順1:上位側でのインターフェース宣言
// com/example/order/OrderRepository.java(上位のパッケージに置く)
package com.example.order;
public interface OrderRepository {
void save(String orderId);
}
インターフェースの名前とメソッドは、上位が必要とする言葉(注文を保存する)で決めます。executeInsertのように保存手段を表す名前は、業務側の契約として適切か見直します。原則Bの確認では、名前だけでなく、引数や戻り値がDB固有の型に依存していないかも調べます。
手順2:下位による抽象の実装と上位への注入
// com/example/infra/MySqlOrderRepository.java(下位)
package com.example.infra;
import com.example.order.OrderRepository;
public class MySqlOrderRepository implements OrderRepository {
@Override
public void save(String orderId) {
System.out.println("INSERT INTO orders VALUES ('" + orderId + "')");
}
}
// com/example/order/OrderService.java(上位)
package com.example.order;
public class OrderService {
private final OrderRepository repository;
public OrderService(OrderRepository repository) {
this.repository = repository;
}
public void place(String orderId) {
repository.save(orderId);
}
}
infraがorderをimportし、orderはinfraを一切importしなくなりました。依存の矢印が逆転した状態です。OrderServiceが依存を引数で受け取っている部分がDI(コンストラクタ注入)です。
手順3:入口での組み立てとテスト用実装への差し替え
// com/example/app/Main.java(具象を知ってよいのは組み立て役だけ)
package com.example.app;
import com.example.infra.MySqlOrderRepository;
import com.example.order.OrderService;
public class Main {
public static void main(String[] args) {
OrderService service = new OrderService(new MySqlOrderRepository());
service.place("A-001");
}
}
// com/example/app/OrderServiceCheck.java(テスト用のメモリ実装に差し替える)
package com.example.app;
import com.example.order.OrderRepository;
import com.example.order.OrderService;
import java.util.ArrayList;
import java.util.List;
public class OrderServiceCheck {
static class InMemoryOrderRepository implements OrderRepository {
final List<String> saved = new ArrayList<>();
@Override
public void save(String orderId) {
saved.add(orderId);
}
}
public static void main(String[] args) {
InMemoryOrderRepository fake = new InMemoryOrderRepository();
new OrderService(fake).place("A-001");
System.out.println(fake.saved);
}
}
具象クラスのnewはmainのような入口(コンポジションルート)に集めます。テストではDBを立てずにメモリ実装を渡せるため、OrderServiceの業務ルールだけを検証できます。実行すると、MainはINSERT INTO orders VALUES ('A-001')を出力し、メモリ実装のsavedは[A-001]になりました。
PythonでのDIP|Protocolによる抽象の定義
インターフェース構文が無い言語でもDIPは成り立ちます。原典自体、C言語のstdio.h(getcharとputchar)を、クラスを使わない依存逆転の例に挙げています。Pythonではtyping.Protocol(PEP 544、Python 3.8で追加)を上位側に置くのが素直です。Protocolは構造的部分型なので、下位の実装はProtocolを継承しなくても、同じメソッドを持てば型チェッカーに互換と判定されます。
from typing import Protocol
class OrderRepository(Protocol): # 上位側に置く抽象
def save(self, order_id: str) -> None: ...
class OrderService:
def __init__(self, repository: OrderRepository) -> None:
self._repository = repository
def place(self, order_id: str) -> None:
self._repository.save(order_id)
class InMemoryOrderRepository: # Protocol を継承しなくてよい
def __init__(self) -> None:
self.saved: list[str] = []
def save(self, order_id: str) -> None:
self.saved.append(order_id)
repo = InMemoryOrderRepository()
OrderService(repo).place("A-001")
print(repo.saved) # ['A-001']
Python 3.9.6で実行し、['A-001']が出力されることを確認しました。TypeScriptのinterfaceも構造的型付けなので、同じ形で書けます。C#(Unityを含む)はJavaと同じくinterfaceを上位側のアセンブリや名前空間に置き、実装クラスがそれを実装します。
DI(依存性の注入)との違い|比較表と混同の理由
DIとDIPは略語が似ているうえ、上のコードのように同じ場所に一緒に現れるため混同されます。DIという呼び名は、Martin Fowlerが2004年1月に公開した「Inversion of Control Containers and the Dependency Injection pattern」で広まりました。DIPより8年ほど後の言葉です。
DIの3つの注入方式
Fowlerは注入の主な方式として、コンストラクタ注入・セッター注入・インターフェース注入の3つを挙げました。この記事では、コンストラクタ注入・セッター注入に加え、処理の引数で依存を都度渡すメソッド注入を比較します。これはFowlerが挙げた3方式とは異なる整理です。Spring Frameworkの公式リファレンスは、必須の依存にはコンストラクタ、任意の依存にはセッターを使うのを目安とし、Springチームとしてはコンストラクタ注入を推奨すると書いています(不変オブジェクトにでき、必須の依存がnullにならないため)。Springでの書き方はSpringのDI(依存性注入)の解説で扱っています。
| 方式 | 渡し方 | 向いている依存 |
|---|---|---|
| コンストラクタ注入 | 生成時の引数 | 必須・不変の依存(既定) |
| セッター注入 | setterで後から | 既定値を持てる任意の依存 |
| メソッド注入 | 処理の引数で都度 | その処理だけで使う依存 |
DIとDIPの比較表
| 観点 | DI(依存性の注入) | DIP(依存性逆転の原則) |
|---|---|---|
| 正体 | 実装手法 | 設計原則(SOLIDのD) |
| 扱う問題 | 依存をどこで作りどう渡すか | 依存の矢印をどちらへ向けるか |
| 初出 | Fowler 2004年 | Martin 1996年 |
| 見る場所 | コンストラクタ引数など | 上位から下位の具象への参照 |
| 単独で成立するか | 具象を渡してもDI | コンテナ無しの手動配線でも成立 |
DIを使ってもDIPにならない2つのケース
1つ目は、具象クラスをそのまま注入するケースです。OrderService(MySqlOrderRepository repository)と書けば依存は外から渡されていますが、orderはinfraの具象をimportしたままです。2つ目は、インターフェースを下位パッケージに置くケースで、前の章で説明したとおり矢印は逆転しません。どちらも「DIしているからDIPも守れている」という思い込みから生まれます。DIPを満たしているかは、上位から下位の具象型への参照と、抽象の引数・戻り値などに詳細への依存が漏れていないかを確認して判断します。
IoC・DIコンテナとの関係
IoC(制御の反転)は、処理の流れやオブジェクト生成の主導権を自分のコードからフレームワークへ渡す広い概念で、DIはその一形態です。Fowlerは前述の記事で「Inversion of Controlは一般的すぎる用語で混乱を招く」と書き、IoCの推進者たちと議論してDependency Injectionという名前に落ち着いたと説明しています。DIコンテナ(Spring、Google Guice、Dagger/Hiltなど)は、手順3でmainに書いた組み立てを自動化する仕組みで、DIPを守るために必須ではありません。Springのコンテナの仕組みはIoCコンテナの解説にまとめています。
3つの関係を一言で並べると、DIPが「どちらを向けるか」の原則、DIが「どう渡すか」の手法、DIコンテナが「渡す作業の自動化」です。名前に「依存性」「逆転(反転)」が重なっているだけで、レイヤーが違います。
ArchUnitによるDIP違反の検出
DIPはレビューで「import文を見る」と言っても、パッケージが増えると見落とします。JavaならArchUnitで依存の向きをテストにできます。ArchUnitのユーザーガイドにあるnoClasses().that().resideInAPackage(...).should().dependOnClassesThat().resideInAPackage(...)で、「orderはinfraに依存しない」と書きます。
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
public class DipRuleCheck {
public static void main(String[] args) {
JavaClasses classes = new ClassFileImporter().importPath(args[0]);
ArchRule rule = noClasses().that().resideInAPackage("..order..")
.should().dependOnClassesThat().resideInAPackage("..infra..");
rule.check(classes);
System.out.println("OK: order does not depend on infra");
}
}
ArchUnit 1.5.1でこの規則を4つの版に当てた結果が次の表です。通常はJUnitのテストとして書きますが、ここでは結果を見やすくするためmainからcheckを呼んでいます。実行にはArchUnit本体(今回はarchunit-1.5.1.jar)と依存ライブラリのslf4j-apiをクラスパスに追加し、第1引数に検査対象のコンパイル済みクラスのディレクトリを指定します。
| 版 | 結果 | 検出された依存 |
|---|---|---|
| 上位が具象をnew(違反) | 違反3件 | コンストラクタ呼び出し・フィールド型・メソッド呼び出し |
| 具象をコンストラクタ注入(DIのみ) | 違反3件 | 引数の型・フィールド型・メソッド呼び出し |
| インターフェースをinfraに置く | 違反3件 | infra.OrderRepositoryへの依存 |
| インターフェースをorderに置く | 合格 | なし |
DIだけの版も、インターフェースを下位に置いた版も、規則の上では最初の違反版と同じく失敗します。今回の4つの版では、抽象を上位側に置いた版だけが合格しました。ただし、この規則が検査するのは指定パッケージ間の依存であり、抽象が業務上適切かまで保証するものではありません。層の数が多いプロジェクトでは、同じガイドのlayeredArchitecture()で層ごとに「どの層からのアクセスを許すか」を定義する書き方もあります。Pythonではimport-linterが同じ役割を持ち、ヘキサゴナルアーキテクチャの解説で設定例を扱っています。Goでの検査はgo-cleanarchとgo-arch-lint・ArchUnitの使い分けを参照してください。
DIPのデメリットと守らなくてよい場面
DIPの代償は、インターフェースと実装の2ファイルに分かれることと、コードを読むときにIDEの「定義へ移動」がインターフェースで止まることです。実装が1つでも、詳細の変更から業務ルールを隔離する効果はあります。変更の隔離もテストでの差し替えも必要ない境界では、抽象を増やすコストが利点を上回ります。次の3つの場面では、具象への直接依存を選んでかまいません。
- 詳細が変わる見込みも、テストで置き換える必要もない依存:社内の計算ユーティリティなど。将来の差し替えに備えた抽象はYAGNI(ヤグニ)原則に反し、必要になった時点でインターフェースを抽出すれば足ります。
- 変わらない標準ライブラリや値オブジェクト:
StringやLocalDateのような安定した型を、DIPのためだけに独自の抽象で包む必要は通常ありません(Listはそれ自体がJava標準のインターフェースで、そのまま依存してよい抽象です)。DIPが守ろうとしているのは「変わりやすい詳細」からの隔離です。 - 使い捨てのスクリプトや検証用の小さなプログラム:層そのものが無いので、上位と下位を分ける意味がありません。
反対に、DB・外部API・メール/SMS送信・決済・時刻(Clock)のように「本番とテストで実装を替えたい」「将来入れ替える可能性が高い」境界は、DIPを適用する価値がはっきりあります。判断に迷ったら、その依存を差し替えるテストを今書く必要があるかで決めると線を引きやすくなります。業務システムでこの境界をどこに引くかは、要件と運用期間で変わります。設計段階から相談したい場合は、当社のフルスクラッチ開発で対応しています。
DIPは単独で使うより、ほかのSOLID原則と組み合わせて効きます。上位が抽象に依存していれば、新しい実装を足すだけで機能を拡張でき、既存コードを変更しないオープンクローズドの原則(OCP)を支えます。DIPを層全体に広げた設計がヘキサゴナルアーキテクチャやクリーンアーキテクチャの「依存性ルール」です。
よくある質問
依存性逆転の原則とは簡単に言うと何ですか?
業務ルールを持つ上位のコードが、DBや外部サービスといった下位の実装を直接参照せず、上位側で定めたインターフェースだけに依存するようにする設計原則です。下位の実装がそのインターフェースを実装するため、依存の向きが従来と逆になります。
DIとDIPは同じものですか?
違います。DIは依存オブジェクトを外から渡す実装手法、DIPは依存の向きを抽象へ向ける設計原則です。DIはDIPを実現するときによく使われますが、具象クラスを注入すればDIでもDIPは満たしません。
依存性逆転の原則のデメリットは何ですか?
インターフェースと実装にファイルが分かれ、コードを追うときにインターフェースで一段止まることです。実装が1つでも変更の隔離に役立ちますが、変更の隔離もテストでの差し替えも不要なら、抽象を増やすコストが利点を上回ります。
依存性注入のデメリットは何ですか?
どの実装が渡されているかが、利用側のコードだけでは読めなくなることです。DIコンテナで自動配線すると、組み立ての場所が設定やアノテーションに散らばり、追跡がさらに難しくなります。小規模ならmainで手動配線するほうが流れを追えます。
DIPはJava以外の言語でも使えますか?
使えます。原典自体がC言語のstdio.hを依存逆転の例に挙げています。PythonはProtocol、TypeScriptはinterface、C#(Unityを含む)はinterfaceを上位側に置けば同じ構造になります。