依存関係逆転の原則ってなにが逆転してるのか整理してみた。
はじめに
依存関係逆転の原則を守ることで、インターフェースや抽象クラスを利用して間接的に依存させるところまではイメージできたけれど、「逆転」という言葉がうまくイメージできなかった。
通常の設計と 適用後の違いを整理して、どのように「依存の方向が逆転するのか」 を自分なりに整理してみた。
依存関係逆転の原則とは
依存関係逆転の原則(DIP:Dependency Inversion Principle)は、オブジェクト指向設計におけるSOLID原則の一つで、 特に依存関係の構築方法に焦点を当てた設計原則。
この原則は、高レベルモジュールが低レベルモジュールに依存するのではなく、抽象に依存すべきであると書かれている。
通常の設計では、ビジネスロジック(高レベル)がデータベースやAPI(低レベル)を直接利用することが多いため、低レベルモジュールに依存してしまう。
高レベルモジュール:ビジネスロジック
低レベルモジュール:データベースアクセスやAPI呼び出し
高レベルモジュールが直接低レベルモジュールに依存すると、低レベルの変更(データベースを MySQL から PostgreSQL に変更するなど)が、ビジネスロジックにも影響を及ぼしてしまう。
その結果、アプリの拡張性が低くなり、修正のたびに多くの変更が必要になってしまうので、高レベルモジュールが低レベルモジュールを直接使用するのではなく、抽象クラスやインターフェースを利用して間接的に使用する。
→ 既存のプログラムの修正をしないで変更や追加の対応がしやすくなる。
依存関係逆転の原則を守ることで、システムの保守性と拡張性が向上し、変更に強い設計が実現できる。
依存関係逆転の原則を適用すべきケース
メソッド内にデータベースアクセスのロジックに直接依存してしまっている
MySQL を PostgreSQL に移行する際に、すべてのデータアクセス部分を書き換えないといけなくなる
// 低レベルモジュール(データベースアクセス)
class MySQLDatabase {
query(sql: string): any {
console.log("Executing MySQL Query:", sql);
return {};
}
}
// 高レベルモジュール(ビジネスロジック)
class UserService {
private db: MySQLDatabase; // 直接依存している
constructor() {
this.db = new MySQLDatabase(); // 具体的な実装に依存
}
getUser(id: number): any {
return this.db.query(`SELECT * FROM users WHERE id = ${id}`);
}
}
-
UserService
がMySQLDatabase
に直接依存しており、データベースを変更(例:PostgreSQLへ移行)する際にUserService
のコードも修正が必要になる。 - 高レベルモジュール (
UserService
) が低レベルモジュール (MySQLDatabase
) に依存しているため、変更に弱い設計になっている。
// ① 抽象クラス・インターフェースを定義(高レベルが依存する)
interface Database {
query(sql: string): any;
}
// ② 低レベルモジュール(データベース実装)が抽象に依存
class MySQLDatabase implements Database {
query(sql: string) {
console.log("Executing MySQL Query:", sql);
return {};
}
}
class PostgreSQLDatabase implements Database {
query(sql: string) {
console.log("Executing PostgreSQL Query:", sql);
return {};
}
}
// ③ 高レベルモジュール(ビジネスロジック)は抽象(Database)を利用する
class UserService {
private db: Database;
constructor(db: Database) {
this.db = db;
}
getUser(id: number): any {
return this.db.query(`SELECT * FROM users WHERE id = ${id}`);
}
}
// ④ 具体的なデータベースを注入(依存の注入:Dependency Injection)
const mysqlDb = new MySQLDatabase();
const userService = new UserService(mysqlDb);
userService.getUser(1);
ビジネスロジックが特定のフレームワークやライブラリに依存している
UserService
クラスが axios
を直接使って外部 API を呼び出している場合、API の仕様変更や fetch
への移行時に UserService
の修正が必要になってしまう。
import axios from "axios";
class UserService {
async getUser(id: number) {
const response = await axios.get(`https://api.example.com/users/${id}`);
return response.data;
}
}
// 抽象(インターフェース)
interface HttpClient {
get(url: string): Promise<any>;
}
// 低レベルモジュール(Axios の実装)
class AxiosHttpClient implements HttpClient {
async get(url: string) {
const response = await axios.get(url);
return response.data;
}
}
// 高レベルモジュール(ビジネスロジック)
class UserService {
private httpClient: HttpClient;
constructor(httpClient: HttpClient) {
this.httpClient = httpClient;
}
async getUser(id: number) {
return await this.httpClient.get(`https://api.example.com/users/${id}`);
}
}
// axios を使用
const userService = new UserService(new AxiosHttpClient());
userService.getUser(1);
-
fetch
などの他の HTTP クライアントに変更しても、UserService
の修正は不要 - モックの
HttpClient
を作れば、テストが簡単になる
依存関係逆転の原則の主なルール
- 高レベルモジュールは低レベルモジュールに依存してはならない。両者は抽象に依存すべきである。
- 高レベルモジュール:ビジネスロジック
- 低レベルモジュール:データベースアクセスやAPI呼び出し
→ データベースアクセスなどの変更がビジネスロジックへ影響を与えないように、インターフェースや抽象クラスを介して依存するようにする。
- 抽象は詳細に依存してはならない。詳細が抽象に依存すべきである。
→ インターフェースや抽象クラスは具体的な実装を前提に作られるのではなく、詳細(実装)が抽象を満たす形で設計されるべき。
依存関係逆転の原則を適用するメリット
- 低レベルモジュール(データベースアクセスや外部API)を変更しても、高レベルモジュール(ビジネスロジック)に影響を与えにくくなる
- 依存関係が明確になり、モジュールのテストが容易になる(モックを使いやすくなる)
なぜ「逆転」と言えるのか?
DIP適用前(従来の設計)
- 高レベル(ビジネスロジック)→ 低レベル(DB)
-
UserService
がMySQLDatabase
に直接依存
適用後
- 低レベル(DB)→ 抽象(インターフェース)← 高レベル(ビジネスロジック)
-
UserService
はDatabaseInterface
に依存し、具体的な DB 実装がDatabaseInterface
に従う
なぜ逆転なのか?
- 通常の設計では「高レベルが低レベルに合わせる」
- DIP適用後は「低レベルが高レベルのルールに従う」
- 依存関係の支配関係が逆転するため「依存関係の逆転」と呼ばれる
まとめ
- 通常の設計では:「高レベル → 低レベル」への依存
- DIP適用後は:「低レベルが高レベルの決めたルール(抽象)に依存」
- 依存の方向が「逆転」するので Dependency Inversion(依存関係の逆転)」と呼ばれる
- 支配関係が逆転し、低レベルが高レベルのルールに従う設計になる
- インターフェースや抽象クラスは、低レベルではなく「高レベルが必要とする機能」を基準に設計する
- 「どのデータベースを使うか?」ではなく、「何をしたいのか?」を考えて抽象化する
Discussion