コンストラクタに具体的なクラスを渡せば依存性逆転原則(DIP)を満たしていると思っているエンジニアは少なくありません。しかし、それは誤解です。DIPの本質は「何を渡すか」ではなく「何に依存するか」という設計の向きにあります。
SOLID原則(オブジェクト指向設計の5つの指針をまとめた頭字語)の最後の1つであるDIPは、次の2つのルールで構成されます。まず、上位モジュールは下位モジュールに依存してはならない。次に、抽象は詳細に依存してはならず、詳細が抽象に依存すべきだということです。
上位モジュールとはビジネスロジックを担うクラスのことで、下位モジュールとはデータベース接続やメール送信など具体的な処理を担うクラスです。DIPが求めるのは、この2者が直接結びつくのではなく、両者の間にインターフェース(実装の契約だけを定めた抽象的な型定義)を挟むことです。
密結合がもたらす問題を具体コードで確認する
シード記事のPHPコードを例に取ります。次のような設計は、DIPに違反した典型的なパターンです。
class NotificationManager {
private $database;
public function __construct() {
$this->database = new MySQLDatabase();
}
}この実装では、NotificationManager(上位)がMySQLDatabase(下位)を直接インスタンス化しています。MySQLをMongoDBに切り替えたい場合、NotificationManagerのコードを書き換える必要が生じます。ビジネスロジックがデータベースの選択に引きずられる形です。テスト時も実際のDBに接続せざるを得ず、テストの速度と独立性が損なわれます。
インターフェースを挟むと何が変わるか
DIPに従った設計では、まずインターフェースを定義します。
interface LoggerInterface {
public function log(string $message): void;
}
class MySqlLogger implements LoggerInterface {
public function log(string $message): void { /* MySQL書き込み */ }
}
class NotificationManager {
public function __construct(private LoggerInterface $logger) {}
public function sendAlert($message) {
$this->logger->log($message);
}
}NotificationManagerはLoggerInterfaceだけを知っています。MySQLかMongoDBか、あるいはテスト用のモックかは、外部から差し込まれる実装が決めます。上位モジュールが抽象に依存し、下位モジュールも同じ抽象に従う。これが「依存の向きを逆転させる」という意味です。
DIとDIPの混同がなぜ起きるのか
依存性注入(DI)は、オブジェクトを外部から渡す実装テクニックです。一方、DIPは「依存先を抽象にすべき」という設計原則です。両者は別の概念ですが、DIPを実現する手段としてDIが使われるため、セットで語られがちです。
コンストラクタ引数に具体クラスを渡している場合、DIは実践していますがDIPは満たしていません。依存先が具体クラスのままだからです。インターフェース型で受け取ることで初めてDIPが成立します。
TypeScriptでも同様の構造は頻繁に登場します。Reactのカスタムフックや、Next.jsのサーバーアクションでAPIクライアントを差し替えたいケースなど、フロントエンドでも具体的なfetch実装に直接依存するのではなく、リポジトリインターフェースを挟む設計は有効です。テスト時にはMSW(Mock Service Worker)などで実装を差し替え、本番時には実際のHTTPクライアントを注入するパターンがこれに当たります。
DIPが解決する3つの場面
- 技術スタックの変更: StripeからPayPayへの決済基盤切り替え、SendGridから別のメールサービスへの移行など、外部ベンダーの変更をビジネスロジックに波及させない
- テストの高速化: モック実装を差し込むことでDBやネットワークに依存しないユニットテストが書ける
- 並行開発: インターフェースさえ合意すれば、上位と下位の実装を別チームが同時に進められる
DIPは静的型付けのある言語(TypeScript、Java、C#、PHP)で最も恩恵が明確ですが、動的型付けのPythonでもProtocolやABC(抽象基底クラス)を使って同じ構造を実現できます。
フロントエンド開発においても、状態管理ライブラリや非同期処理の実装をコンポーネントに直書きするかどうかで、将来の移行コストが大きく変わります。ZustandからJotaiへ、あるいはREST APIからGraphQLへの切り替えを想定した設計では、DIPの考え方は実用的な指針になります。抽象に依存するコードは、特定技術の流行り廃りに左右されにくいという性質を持ちます。