コンテンツにスキップ

05. 依存関係逆転の原則(DIP: Dependency Inversion Principle)

Clean Architecture 達人に学ぶソフトウェアの構造と設計の第 11 章を読もう。

概要

依存関係逆転の原則(DIP: Dependency Inversion Principle)とはモジュールを疎結合にするための原則である。

実際に依存する向きクラス図上の依存の向きが逆になっているとき、依存関係逆転の原則に従っているという。

具象に依存するのではなく、抽象に依存するように設計したとき、依存関係逆転の原則に従っているというという方がわかりやすいかもしれない。

もう少し詳しい概要

以下のようにアプリケーションAppがユーザーのデータベースUserRepositoryに依存することはよくある。

uml diagram

依存関係逆転の原則に従うと、インターフェースを用意することになる。

uml diagram

このときAppIUserRepositoryをアプリケーション層というくくりで見る。

uml diagram

最初のクラス図と見比べると、層間の矢印の向きが逆転していることがわかる。これが依存関係逆転である。

具体例

だから何?という感じなので、依存関係逆転の原則が活きる具体例を紹介する。

ユーザーのデータベースにアクセスしてユーザーの一覧を画面に出力するアプリケーションを考えよう。

とりあえず作る

まず単一責任の原則を踏まえると、データベースアクセスの担当と、結果の画面表示の担当は分けるべきだ。

今回はそれぞれUserRepositoryAppとしてみた。

src/user-repository.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
export class UserRepository {
  getUsers() {
    // ここに100行くらいの難しい処理があって、本番環境のデータベースにアクセスしている
    // 以下は複雑な処理の結果
    return [
      { name: "ユーザー1" },
      { name: "ユーザー2" },
      { name: "ユーザー3" },
    ];
  }
}
src/app.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { UserRepository } from "./user-repository";

export class App {
  private userRepository = new UserRepository();

  run() {
    console.log("ユーザーの一覧");
    console.log(
      this.userRepository
        .getUsers()
        .map((user) => user.name)
        .join("\n")
    );
  }
}
src/main.ts
1
2
3
4
import { App } from "./app";

const app = new App();
app.run();

このコードの依存関係をクラス図で表現すると以下になる。

uml diagram

src/main.tsの実行結果
1
2
3
4
ユーザーの一覧
ユーザー1
ユーザー2
ユーザー3

完成した。

何がまずいのか

Appの単体テストが難しい。

あるクラスが実クラス(抽象クラスでない普通のクラス)に依存することを密結合といい、一般的に良くないことであるとされる。

今回の例でいうとAppというクラスがUserDatabaseという実クラスに依存しており、密結合になっている。

密結合になっているために、Appの単体テストが難しい。

Appを単体テストするときはリポジトリ層をモックに差し替えるべきだ。しかし差し替えるすべがない。

どうしたら良いのか

密結合を改善するために、依存関係逆転の原則を適用する。

依存関係逆転の原則によると、Appは抽象的なインターフェースに依存すべきなのだ。

依存関係逆転の原則を適用する

AppUserRepositoryという実クラスに依存させるのでなく、IUserRepositoryというインターフェースに依存させるようにする。

src/i-user-repository.ts
1
2
3
export interface IUserRepository {
  getUsers(): { name: string }[];
}
src/user-repository.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import { IUserRepository } from "./i-user-repository";

export class UserRepository implements IUserRepository {
  getUsers() {
    // ここに100行くらいの難しい処理があって、本番環境のデータベースにアクセスしている
    // 以下は複雑な処理の結果
    return [
      { name: "ユーザー1" },
      { name: "ユーザー2" },
      { name: "ユーザー3" },
    ];
  }
}
app.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import { IUserRepository } from "./i-user-repository";
import { UserRepository } from "./user-repository";

export class App {
  private userRepository: IUserRepository = new UserRepository();

  run() {
    console.log("ユーザーの一覧");
    console.log(
      this.userRepository
        .getUsers()
        .map((user) => user.name)
        .join("\n")
    );
  }
}
src/main.ts
1
2
3
4
import { App } from "./app";

const app = new App();
app.run();

依存関係を表すクラス図はこうなる。

uml diagram

src/main.tsの実行結果
1
2
3
4
ユーザーの一覧
ユーザー1
ユーザー2
ユーザー3

依存関係は逆転したのだが、結局AppUserRepositoryに紐づくように固定されているため、まだ単体テストを行うことができない。

Appの単体テストを可能にするにはApp依存性注入(Dependency Injection)の仕組みを取り入れる必要がある。

依存性注入(Dependency Injection)というのはクラスが依存しているクラスを外部から設定することだ。

今回でいうとApp.userRepositoryの実体を外から設定できるようにする。

依存性注入を実現する

依存性注入の実現方法として Constructor Injectionを採用する。

なんのことはなく、Appのインスタンスを生成するときにUserRepositoryのインスタンスを生成してやって、メンバーに設定するだけだ。

src/app.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { IUserRepository } from "./i-user-repository";

export class App {
  constructor(private userRepository: IUserRepository) {}

  run() {
    console.log("ユーザーの一覧");
    console.log(
      this.userRepository
        .getUsers()
        .map((user) => user.name)
        .join("\n")
    );
  }
}
src/main.ts
1
2
3
4
5
import { App } from "./app";
import { UserRepository } from "./user-repository";

const app = new App(new UserRepository());
app.run();
src/main.tsの実行結果
1
2
3
4
ユーザーの一覧
ユーザー1
ユーザー2
ユーザー3

Appの依存する実体をAppを new する側で自由に設定できるようになった。

Appの単体テストコードを書く

これでようやくAppの単体テストのコードを書くことができる。

src/test-code.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { App } from "./app";
import { IUserRepository } from "./i-user-repository";

export class MockUserRepository implements IUserRepository {
  getUsers() {
    return [
      { name: "ダミーのユーザー1" },
      { name: "ダミーのユーザー2" },
      { name: "ダミーのユーザー3" },
    ];
  }
}

const app = new App(new MockUserRepository());
app.run();
src/test-code.tsの実行結果
1
2
3
4
ユーザーの一覧
ダミーのユーザー1
ダミーのユーザー2
ダミーのユーザー3

本番のコードではAppUserRepositoryを使うし、テストコードではAppMockUserRepositoryを使うようになった。

おわりに

というわけでリポジトリ層をモックに置き換えてAppの単体テストができるようになった。

依存関係逆転の原則のメリットを享受するには、依存性注入も併せて取り入れる必要があると思う。

依存性注入の方法として例ではConstructor Injectionを使ったが、他にも DI コンテナのライブラリを利用する方法がある。

依存関係を本格的に整理したい場合は DI コンテナのライブラリを導入するのが良いだろう。