金色の配線パターンが広がる基板の接写
ニュース深掘り

DIPとDIを混同しないための依存性逆転原則入門

目次を見る

コンストラクタに具体的なクラスを渡せば依存性逆転原則(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の考え方は実用的な指針になります。抽象に依存するコードは、特定技術の流行り廃りに左右されにくいという性質を持ちます。

参考

Dependency Inversion Principle (DIP)

この記事について: 本記事は AI を活用して作成し、forva AI 編集部が内容を確認・監修しています。

AI 駆動開発のご相談は forva AI へ。まずはお気軽にどうぞ。