💉

DI(依存性注入) / DIP(依存性逆転の原則) なにそれおいしいの?

2023/05/27に公開

はじめに

こんにちは。
久しぶりに記事を書くやる気が出ましたので、最近学んだDI(依存性注入)とDIP(依存性逆転の原則)について解説してみようと思います。
説明の中で一部コードの例が出てきますが、Javaを例として載せてます。

DI

まずは、DIについてです。
DIは依存性注入と言われており、Dependency Injectionの略になります。
おそらく、プログラミング初学者(もしかしたら中級者)の方は、依存性?注入?と思うかもしれません。

まず、「依存性」についてです。
依存性とは、あるクラスAが他のクラスBを参照しているときに、クラスAはクラスBに依存していると言います。
つまり、クラスAはクラスBがいないと生きていけない。そう、メンヘラクラスAはクラスBに依存してしまっているのです。

例えば、(適当な)コードで表すと以下のような状態です。

class A {
    public void hoge() {
        // クラスAはクラスBを参照している。(=クラスAはクラスBに依存している)
        B b = new B();
	...
    }
}

class B {
    public void foo() {
        ...
    }
}

main {
    A a = new A();
    a.hoge();
}

図で表すと、クラスAからクラスBに矢印が伸びます。

DI

なんで、上記ではダメなのでしょうか?
それは、大きな理由としては以下が挙げられます。

  • テストコードが実装しにくい(時がある)。
  • クラスが再利用しにくい。

テストコードが実装しにくい(時がある)

ここから説明していきます。少し、コードの具体性を持たせてみます。
以下のようなクラスがあったとします。

class UserRepository {
    public List<String> fetchUserNames() {
         String url = "本番DBのURL";
    
         // DBに接続
         DB db = new DB(url);
	 db.connect();
	 
	 // DBからユーザ名の一覧を取得する。
	 List<String> userNames = db.getUserNames();
	 return userNames;
    }
}

class DB {
    private String url;

    DB(String url) {
         this.url = url;
    }
    
    public void connect() {
        // urlを用いてDBに接続する。
    }
    
    public void getUserNames() {
        // ユーザの一覧を取得する。
    }
}

main {
    UserRepository repository = new UserRepository();
    repository.fetchUserNames();
}

この時、UserRepositoryfetchUserNames()のテストコードを書くとき際に困ることがあります。
それは、テスト時は検証環境のDBやモックのDBに接続したいところですが、fetchUserNames()では常に本番環境のDBに接続してしまいます。

クラスが再利用しにくい

上記のコード例を流用します。
極端な例ですが、日本人のユーザ用のDBとアメリカ人のユーザ用のDBがあったとします。
このときに、UserRepositoryクラスを使い回したいところですが、上記のコードだと以下のように2つのクラスに分けざるを得ません。

class JapanUserRepository {
    public List<String> fetchUserNames() {
         String url = "日本人のユーザ用DBのURL";
    
         // DBに接続
         DB db = new DB(url);
	 db.connect();
	 
	 // DBからユーザ名の一覧を取得する。
	 List<String> userNames = db.getUserNames();
	 return userNames;
    }
}

class AmericaUserRepository {
    public List<String> fetchUserNames() {
         String url = "アメリカ用のユーザ用DBのURL";
    
         // DBに接続
         DB db = new DB(url);
	 db.connect();
	 
	 // DBからユーザ名の一覧を取得する。
	 List<String> userNames = db.getUserNames();
	 return userNames;
    }
}

main {
    JapanUserRepository jRepository = new JapanUserRepository();
    jRepository.fetchUserNames();
    
    AmericaUserRepository aRepository = new AmericaUserRepository();
    aRepository.fetchUserNames();
}

JapanUserRepositoryAmericaUserRepositoryは基本やっていることは同じですが、DBのURLが違うだけで無駄なクラスが増えてしまいます。

では、どう改善するか?

結論から言うと、上記の問題を解決するには、UserRepositoryのコンストラクタにDBクラスを渡してやることです。外からクラスを渡すということです。
この例でいう、「外からクラスを渡す」を「注入」と言います。
つまり、依存性を外から注入することをDIというのです。

コードを改善すると以下のようになります。

class UserRepository() {
    private DB db;
    
    UserRepository(DB db) {
        this.db = db;
    }

    public List<String> fetchUserNames() { 
         // DBに接続
	 db.connect();
	 
	 // DBからユーザ名の一覧を取得する。
	 List<String> userNames = db.getUserNames();
	 return userNames;
    }
}

main {
    UserRepository repository = new UserRepository(new DB("URL"));
    repository.fetchUserNames();
}

UserRepositoryが接続するDBのURLを知っていたことが大きな原因でした。
UserRepositoryはURL(DBの詳細情報)を知らなくても問題ないので、UserRepositoryのコンストラクタでDBクラスを渡すようにしています。

テストコード時はUserRepositoryのインスタンスを生成するときに渡すDBクラスにURLを指定できるので、さまざまな環境へ接続することができます。
また、異なるDBインスタンスをコンストラクタに渡すことで、UserRepositoryを使い回すことができるようになりました。

デメリット

DIのデメリットも記載しておきます。
(個人的な)DIのデメリットをあげるとしたら、以下が挙げられます。

  • DIは概念的に少し複雑であり、初めての開発者にとっては学習コストがかかる。

DIP

続いて、DIPについてです。
DIPは依存性逆転の原則と言われており、Dependency Inversion Principleの略になります。
簡単に言えば、DIの注入方向が逆になるようなイメージです。

例えば、(適当な)コードで表すと以下のような状態です。

class A() {
    private B b;
    
    A(B b) {
        this.b = b;
    }

    public void hoge() {
        // クラスAはクラスBを参照している。(=クラスAはクラスBに依存している)
        B b = new BImpl();
	...
    }
}

Interface B {
    public void foo() {}
}

class BImpl implements B {
    public void foo() {
        ...
    }
}

main {
    A a = new A(new BImpl());
    a.hoge();
}

ここで、なぜ依存性が逆転しているかは図で見るとわかりやすいと思います。

上記では、クラスAはインターフェースBに依存しており、クラスBImpleには依存してません。
クラスBImpleはインターフェースBに依存しているので、「クラスAはインターフェースB」をひとまとめに見ると、依存の方向が逆方向になっているのがわかります。
※逆になっていると言ってもクラスBImpleがクラスAに依存しているわけではないのでそこはご注意を。

どうしてこんなことをやっているかというと、できるだけモジュール(ここではクラス)同士の結合度を小さくするためです。
もちろんDIPはDIの一部なので、テストコードが書きやすいというメリットもあります。
モジュール同士の結合度を小さくすると柔軟なアーキテクチャを構築することができます。また、各モジュールの役割を分割できるのでコードの改修もしやすくなると思います。

デメリット

DIPのデメリットも記載しておきます。
(個人的な)DIPのデメリットをあげるとしたら、以下が挙げられます。

  • テストが不要ならそこまでの効果はない。
  • DIは概念的に少し複雑であり、初めての開発者にとっては学習コストがかかる。
  • 抽象化やインターフェースの導入が必要になるため、設計や実装が難しくなる。
  • (あまり気にしなくても良いが)ビルド時、実行時に依存関係を解決するためのオーバーヘッドが発生する場合がある。
    • わずかではあるが、パフォーマンスに影響を与える可能性がある。

まとめ

この記事では、DI(依存性注入)とDIP(依存性逆転の原則)について解説しました。
DIをDIPを理解して、柔軟に設計、実装ができ、テストコードも実装しやすくなるので試してみてください!

Discussion