依存性逆転の原則(DIP)/依存性の注入(DI) は利用すべきではない

依存性逆転の原則(DIP)/依存性の注入(DI) は利用すべきではない

はじめに

SOLID原則の「D」として知られる依存性逆転の原則(DIP: Dependency Inversion Principle)と、その実装手法である依存性の注入(DI: Dependency Injection)は、オブジェクト指向設計において重要な原則として広く教えられている。

しかし清水はDIPは、時代遅れであり現代は利用すべきではなく、プログラミング歴の少ない初心者には害悪であると主張する。

DIPの歴史的背景

DIPは1996年、Robert C. Martin(アンクル・ボブ)によって「The C++ Report」誌で発表された。

ここで重要なのは、この原則がC++という言語の文脈で生まれたということだ。

1996年当時のC++開発には以下の問題があった:

  • ヘッダファイルの依存関係がコンパイル時間に直結する
  • 具象クラスへの依存がヘッダのincludeを必要とし、変更時の再コンパイル範囲が拡大する
  • 抽象クラス(インターフェース)への依存に切り替えることで、コンパイル依存性を削減できる

つまり、DIPの本質的な効果は「コンパイル時間の短縮」だった。

現代の言語でDIPの効果は薄い

言語 コンパイル依存性の問題 DIPの実益
C++ (1996年) 深刻 あり
Python 存在しない(インタプリタ言語) ほぼなし
JavaScript/TypeScript 軽微(バンドラーが解決) 限定的
Go 軽微(高速コンパイラ) 限定的
Java/C# 存在するが軽微 限定的

Python/Rubyのような動的型付け言語では、ダックタイピングにより「インターフェースを明示的に定義する」必要すらない。DIPが解決しようとした問題自体が存在しないのだ。

「将来の拡張性」という幻想

DIP/DI推進派は「将来の変更に備えた設計」を主張する。典型的な例:

「今はMySQLだけだが、将来PostgreSQLに対応するかもしれない。だからDatabaseインターフェースを作っておこう」

しかし、多くの場合こうなる

「将来PostgreSQLに対応するかもしれない」
    ↓
実際に追加されるのはRedis(KVS)だった
    ↓
SQLベースのDatabaseインターフェースでは対応不可
    ↓
インターフェースごと再設計
    ↓
事前の抽象化は完全に無駄だった

将来の要件は予測できない。予測できたとしても、予測通りの形では来ない。


コード比較:DIPなし vs DIPあり

MySQL/PostgreSQL両対応が必要な場合を例に比較する。

DIPなし

import mysql.connector
import psycopg2
import psycopg2.extras


class MySQLDatabase:
    def __init__(self, host: str, user: str, password: str, db: str):
        self.conn = mysql.connector.connect(
            host=host, user=user, password=password, database=db
        )

    def execute(self, query: str, params: tuple = ()) -> list:
        cursor = self.conn.cursor(dictionary=True)
        cursor.execute(query, params)
        if query.strip().upper().startswith("SELECT"):
            return cursor.fetchall()
        self.conn.commit()
        return []

    def close(self) -> None:
        self.conn.close()


class PostgreSQLDatabase:
    def __init__(self, host: str, user: str, password: str, db: str):
        self.conn = psycopg2.connect(
            host=host, user=user, password=password, dbname=db
        )

    def execute(self, query: str, params: tuple = ()) -> list:
        cursor = self.conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
        cursor.execute(query, params)
        if query.strip().upper().startswith("SELECT"):
            return cursor.fetchall()
        self.conn.commit()
        return []

    def close(self) -> None:
        self.conn.close()


class UserService:
    def __init__(self, is_mysql: bool, host: str, user: str, password: str, db: str):
        klass = MySQLDatabase if is_mysql else PostgreSQLDatabase
        self.database = klass(host, user, password, db)

    def get_user(self, user_id: int) -> dict:
        rows = self.database.execute(
            "SELECT * FROM users WHERE id = %s", (user_id,)
        )
        return rows[0] if rows else None

    def create_user(self, name: str, email: str) -> None:
        self.database.execute(
            "INSERT INTO users (name, email) VALUES (%s, %s)",
            (name, email)
        )


# 使用側
service = UserService(is_mysql=True, host="localhost", user="root", password="secret", db="myapp")

DIPあり

from abc import ABC, abstractmethod
import mysql.connector
import psycopg2
import psycopg2.extras


class Database(ABC):
    @abstractmethod
    def execute(self, query: str, params: tuple = ()) -> list:
        pass

    @abstractmethod
    def close(self) -> None:
        pass


class MySQLDatabase(Database):
    def __init__(self, host: str, user: str, password: str, db: str):
        self.conn = mysql.connector.connect(
            host=host, user=user, password=password, database=db
        )

    def execute(self, query: str, params: tuple = ()) -> list:
        cursor = self.conn.cursor(dictionary=True)
        cursor.execute(query, params)
        if query.strip().upper().startswith("SELECT"):
            return cursor.fetchall()
        self.conn.commit()
        return []

    def close(self) -> None:
        self.conn.close()


class PostgreSQLDatabase(Database):
    def __init__(self, host: str, user: str, password: str, db: str):
        self.conn = psycopg2.connect(
            host=host, user=user, password=password, dbname=db
        )

    def execute(self, query: str, params: tuple = ()) -> list:
        cursor = self.conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
        cursor.execute(query, params)
        if query.strip().upper().startswith("SELECT"):
            return cursor.fetchall()
        self.conn.commit()
        return []

    def close(self) -> None:
        self.conn.close()


class UserService:
    def __init__(self, database: Database):
        self.database = database

    def get_user(self, user_id: int) -> dict:
        rows = self.database.execute(
            "SELECT * FROM users WHERE id = %s", (user_id,)
        )
        return rows[0] if rows else None

    def create_user(self, name: str, email: str) -> None:
        self.database.execute(
            "INSERT INTO users (name, email) VALUES (%s, %s)",
            (name, email)
        )


def create_database(config: dict) -> Database:
    if config["type"] == "mysql":
        return MySQLDatabase(
            host=config["host"], user=config["user"],
            password=config["password"], db=config["db"]
        )
    elif config["type"] == "postgres":
        return PostgreSQLDatabase(
            host=config["host"], user=config["user"],
            password=config["password"], db=config["db"]
        )
    raise ValueError(f"Unknown database type: {config['type']}")


# 使用側
db = create_database({"type": "mysql", "host": "localhost", "user": "root", "password": "secret", "db": "myapp"})
service = UserService(db)

比較結果

DIPありのコードは以下の問題がある:

  1. コード量が増加(抽象クラス + ファクトリ関数)
  2. UserServiceの利用者は結局どのDatabaseを渡すか知っている必要がある

DIPなしでも「利用側はMySQLかPostgreSQLかを意識せずにUserServiceを使える」という関心の分離は達成されている。

「テストのためにDIが必要」という主張への反論

DBのテストはDB名切り替えで十分

多くのプロジェクトでは、テスト用のデータベースを用意するだけで済む:

DB_NAME=app_test pytest
アプローチ メリット デメリット
DB名切り替え シンプル、実際のDBで動作確認 テストDBのセットアップが必要
DIでモックDB注入 DBなしでテスト可能 実装が複雑化、実DBとの乖離リスク

外部APIのテストはモックサーバーを検討せよ

外部APIをテストする場合も、DIでモッククラスを注入する前にモックサーバーを検討すべきだ:

# モックサーバー(FastAPIで数分で作成可能)
from fastapi import FastAPI

app = FastAPI()

@app.get("/api/users/{user_id}")
def get_user(user_id: int):
    return {"id": user_id, "name": "テストユーザー"}
API_BASE_URL=http://localhost:8000 pytest
アプローチ メリット デメリット
DIでモッククラス注入 コード内で完結 実装が複雑化、実APIとの乖離リスク
モックサーバー 実際のHTTP通信をテスト サーバー起動が必要

優先すべき原則

優先度 原則 内容
1 YAGNI 必要になるまで作るな
2 KISS シンプルに保て
3 DRY 実際に重複したら共通化しろ
4 DIP/DI 上記で解決できない場合のみ検討

初心者に教えるべきこと

DIP/DIを最初から教えるのは有害だ。初心者には以下を教えるべき:

  1. 動くコードを書け
  2. 重複したら共通化しろ
  3. 困ったら直せ

抽象化やDIは「困った時の道具」であり、最初から使うものではない。

結論

依存性逆転の原則(DIP)と依存性の注入(DI)は、1996年のC++における「コンパイル時間短縮」という具体的な問題を解決するために生まれた。

現代の言語ではこの問題が軽微または存在せず、「将来の拡張性」のための事前抽象化は無駄になる場合がほとんどだ。

原則のための原則に従うな。問題を解決するために原則を使え。

この記事をシェア

弊社では、一緒に会社を面白くしてくれる仲間を募集しています。
お気軽にお問い合わせください!