🚀

★★ ★ Java | 依存性注入(DI) | 直行性(Loose Coupling)

に公開

「依存性注入(DI)」と「直行性(Loose Coupling)」は異なる概念だが、密接に関連している。依存性注入(DI)は、コードの直行性を高めるための手段の1つであり、依存性注入(DI)を適用することで、クラス間の依存関係を緩やかにし、コードの可読性やテストの容易さを向上させることができる。

直行性(Loose Coupling)

直行性(Loose Coupling)とは、クラスやモジュール間の依存関係を示す。「直行性が高い」場合は、あるクラスが他のクラスに強く依存していないため、変更が容易で、テストや再利用がしやすくなる。

依存性(Dependency)とは何か?

  • 依存性(Dependency)とは、あるクラスやモジュールが他のクラスやモジュールに依存している状況のこと。
  • 具体的には、あるクラスが他のクラスのインスタンスを使用する場合、「そのクラスは依存関係を持っている」と表現される。
/* 依存性(Dependency) */
/* この場合、"Car"クラス は "Engine"クラス に依存しており、
   "Engine"クラス がなければ "Car"クラス は正常に動作しない。 */

class Engine {
    public void start() {
        System.out.println("Engine started");
    }
}

/* 「この Car クラス は Engine クラス に依存関係を持っている」 */
class Car {
    private Engine engine;

    public Car() {
        this.engine = new Engine(); // 依存性
    }

    public void start() {
        engine.start();
    }
}
  • 例えばこのサンプルコードでは Car クラス が Engine クラス のインスタンスを使用しており、Car クラス は Engine クラス に依存している。Car クラス は、Engine クラス がなければ Car クラス が正常に動作しないという依存性(Dependency)を持っているということになる。

依存性(Dependency)の存在で発生する問題点

依存性が強いと、テストの困難、変更に対する脆弱性、再利用性の低下、システムの複雑化といった問題が発生する。

  1. テストが困難になる。
    • 依存性が強いクラスは、ユニットテストが難しくなる。
    • 特定のクラスに依存している場合、そのクラスの実装に依存してしまうため、テストの独立性が損なわれる。
/* 依存性(Dependency)の存在で発生する問題点 */
/* ①テストが困難になる                      */

class Database {
    public void connect() {
        // データベース接続処理
    }
}

class UserService {
    private Database database;

    public UserService() {
        this.database = new Database(); // 依存性
    }

    public void addUser(String user) {
        database.connect();
        // ユーザー追加処理
    }
}

// テストが困難
public class UserServiceTest {
    public void testAddUser() {
        UserService userService = new UserService(); // Databaseに依存している
        userService.addUser("John Doe");
        // ここでDatabaseの挙動に依存したテストが必要になる
    }
}
  1. 変更に対する脆弱性、メンテナンス性の悪化。
    • 依存性が強いと、依存先のクラスに変更があった場合、依存しているクラスも影響を受けやすくなる。これにより、メンテナンス性が悪くなる。
/* 依存性(Dependency)の存在で発生する問題点 */
/* ②変更に対する脆弱性、メンテナンス性の悪化。*/

// Engineクラスを変更するとCarクラスにも影響が出る
class Engine {
    public void start() {
        System.out.println("Engine started");
    }
}

class Car {
    private Engine engine;

    public Car() {
        this.engine = new Engine(); // 依存性
    }

    public void start() {
        engine.start();
    }
}
  1. 再利用性の低下
    • 依存性が強いクラスは、他のコンテキストで再利用しにくくなる。
    • 特定の依存関係に縛られてしまうため、他のプロジェクトやクラスで使うのが難しくなる。
/* 依存性(Dependency)の存在で発生する問題点 */
/* ③再利用性の低下。*/

// "UserService"クラス は "Logger"クラス に強く依存しているため、別のロギング手法を使いたい場合に再実装が必要となる

class Logger {
    public void log(String message) {
        System.out.println(message);
    }
}

class UserService {
    private Logger logger;

    public UserService() {
        this.logger = new Logger(); // 依存性
    }

    public void addUser(String user) {
        logger.log("User added: " + user);
    }
}
  1. システムの複雑化
    • 依存性が多層に重なると、システム全体が複雑化し、理解しづらくなる。
    • 特に大規模なプロジェクトでは、依存関係の管理が難しくなる。
/* 依存性(Dependency)の存在で発生する問題点 */
/* ④システムの複雑化。*/

// 依存性が多層な状況。大規模なプロジェクトになると依存関係の管理が難しくなる。
// "A"クラス に依存する"B"クラス、"B"クラス に依存する "C"クラス がある場合、"A"クラス をテストするためには "B"クラス と "C"クラス も考慮する必要がある。

class A {
    private B b;

    public A() {
        this.b = new B(); // 依存性
    }
}

class B {
    private C c;

    public B() {
        this.c = new C(); // 依存性
    }
}

class C {
    // 何らかの処理
}

依存性注入(Dependency Injection):DI

依存性の問題を解決するためには、依存性注入(DI:Dependency Injection) や、インターフェースを利用した設計 が有効である。これにより、クラス間の依存関係を緩和し、コードの柔軟性と可読性を向上させることができる。

  • 依存性注入(DI:Dependency Injection) とは、オブジェクトの依存関係を外部から注入する手法。
  • これにより、クラスのインスタンスが自分で依存するオブジェクトを作成するのではなく、外部から提供されるようになる。
  • 依存性注入により、コードのテストやメンテナンスが容易になる。

[DI前] 依存性注入前のコード。依存性が強く変更に対する脆弱性やメンテナンス性が悪い。

/* 依存性(Dependency)の存在で発生する問題点 */
/* ②変更に対する脆弱性、メンテナンス性の悪化。*/

// "Engine"クラス と "Car"クラスの依存性が強く、
// "Engine"クラス を変更すると "Car"クラス にも影響が出る。
class Engine {
    public void start() {
        System.out.println("Engine started");
    }
}

class Car {
    private Engine engine;

    public Car() {
        this.engine = new Engine(); // 依存性が強い。
    }

    public void start() {
        engine.start();
    }
}

[DI後] 依存性注入後のコード。直行性の高いコードへ改善されており、脆弱性やメンテナンス性が改善されている。

/* 依存性注入(DI:Dependency Injection)の具体例 */

class Engine {
    public void start() {
        System.out.println("Engine started");
    }
}

class Car {
    private Engine engine;

    /* クラスのインスタンスが自分で依存するオブジェクトを作成するのではなく、
       外部から提供されるようにすることでDIを実現させる */
    // コンストラクタで依存性を注入(DI)し、直行性の高いコードに改善。
    public Car(Engine engine) {  // コンストラクタで依存性を注入(DI)できるように改善
        this.engine = engine; // 依存性
    }

    public void start() {
        engine.start();
    }
}

// DI 使用例
public class Main {
    public static void main(String[] args) {
        Engine engine = new Engine(); // 依存性を外部で作成(DI)
        Car car = new Car(engine); // 依存性を注入(DI)
        car.start();
    }
}
  • 依存性注入(DI)を使ったサンプルコードの例。
  • Car クラス が Engine クラス に依存している場合、Engine クラス のインスタンスをコンストラクタを通じて注入することができる。
  • このように依存性注入(DI:Dependency Injection)とは、外部から依存オブジェクトを注入すること。クラスの独立性を高めてコードの柔軟性やテストの容易さが向上し、大規模なアプリケーションの開発やメンテナンスが効率的になる。コンストラクタやセッターを通じて依存性を注入することが一般的。

/* 依存性が多層な状況でのDI適用例 */
/* コンストラクタを通じて依存関係を注入している。*/

class C {
    public void doSomething() {
        // 何らかの処理
        System.out.println("C is doing something.");
    }
}

/* クラス B は、コンストラクタで C のインスタンスを受け取る(DI) */
class B {
    private C c;

    // Cをコンストラクタで注入
    public B(C c) {
        this.c = c;
    }

    public void doSomething() {
        c.doSomething();
    }
}

/* クラス A は、コンストラクタで B のインスタンスを受け取る(DI) */
class A {
    private B b;

    // Bをコンストラクタで注入
    public A(B b) {
        this.b = b;
    }

    public void start() {
        b.doSomething();
    }
}

// 使用例
public class Main {
    public static void main(String[] args) {
        C c = new C(); // Cのインスタンスを作成
        B b = new B(c); // CをBに注入
        A a = new A(b); // BをAに注入
        a.start(); // Aのメソッドを呼び出す
    }
}
  • 依存性が多層な状況で、依存性注入(DI)を使ったサンプルコードの例。

  • 「クラス B は、コンストラクタで C のインスタンスを受け取る」,「クラス A は、コンストラクタで B のインスタンスを受け取る」ようにして、依存関係を注入している。

  • A、B、C のクラスをテストする際に、それぞれの依存関係をモックやスタブに置き換えることが容易になる。例えば、B や C の実装を変更しても、A のテストに影響を与えにくくなる。

  • 依存性注入(DI)を使用することで、異なる実装を簡単に差し替えることができ、コードの柔軟性が向上する。たとえば、C の別の実装を作成し、B に注入することができる。

/* C の別の実装(異なる処理を行うことを想定)を作成し、B に注入する例。 */
/* C の別の実装として CAlternative クラスを作成している。 */

class C {
    public void doSomething() {
        System.out.println("C is doing something.");
    }
}

/* C の別の実装(異なる処理を行うことを想定)として CAlternative クラスを作成 */
class CAlternative {
    public void doSomething() {
        System.out.println("CAlternative is doing something different.");
    }
}

class B {
    private C c;

    // Cをコンストラクタで注入
    public B(C c) {
        this.c = c;
    }

    public void doSomething() {
        c.doSomething();
    }
}

class A {
    private B b;

    // Bをコンストラクタで注入
    public A(B b) {
        this.b = b;
    }

    public void start() {
        b.doSomething();
    }
}

// 使用例
public class Main {
    public static void main(String[] args) {
        // Cのインスタンスを作成してBに注入
        C c = new C();
        B b = new B(c);
        A a = new A(b);
        a.start(); // "C is doing something." と表示される

        // CAlternativeのインスタンスを作成してBに注入
        CAlternative cAlternative = new CAlternative();
        B bAlternative = new B(cAlternative);
        A aAlternative = new A(bAlternative);
        aAlternative.start(); // "CAlternative is doing something different." と表示される
    }
}

上記のコードは、最初に C のインスタンスを作成しB と A に注入して start メソッドを呼び出して次に、CAlternative のインスタンスを作成し、同様に B と A に注入して start メソッドの呼び出しを行っている。以下のような出力が得られる。

C is doing something.
CAlternative is doing something different.

もともとは依存性が多層でメンテナンス性の悪い設計だったが、依存性注入(DI)を利用することで、異なる実装を簡単に切り替えられる柔軟でメンテナンス性良い設計へと改善されている。コードの再利用性やテストの容易さが向上している。


依存性の注入パターンと注入度

一般的には、 "Constructor Injection" が推奨されることが多い。

DI手法 依存性の注入度 特徴
Constructor Injection High Injection 依存性を private かつ不変にできる。明示的、不可変、テストが容易。
Method Injection 中程度の注入 依存性を private にできるが、可変になる。柔軟性が高いが、初期化が不確実な場合がある。
Interface Injection 中程度の注入 柔軟性があるが、設計が複雑になることがある。
List Injection, Collection Injection 中程度の注入 複数依存関係を一度に注入、柔軟性がある。
Field Injection Low Injection 簡潔だが、テストが難しい。
Property Injection Low Injection 設定の柔軟性があるが、初期化が不確実な場合がある.
Setter Injection Low Injection 依存性を外部に公開する必要があり、可変にもなる。柔軟性があるが、依存関係が不明瞭になりやすい。

  1. Constructor Injection:高い注入
    • 依存関係がコンストラクタの引数として明示的に示されるため、どの依存関係が必要かが明確。
    • 不変性が保証され、インスタンス生成時にすべての依存関係が解決されるため、オブジェクトの状態が予測可能。
    • テストが容易で、依存関係を簡単にモックやスタブに置き換えることができる。
/* Constructor Injection */
/* コンストラクタインジェクションでは、 */
/* 依存オブジェクトをコンストラクタの引数として受け取る */

class A {
    private final B b;

    // Bをコンストラクタで注入
    public A(B b) {
        this.b = b;
    }
}

class B {
    private final C c;

    // Cをコンストラクタで注入
    public B(C c) {
        this.c = c;
    }
}

class C {
    // 何らかの処理
}

C c = new C();
B b = new B(c);
A a = new A(b);


  1. Method Injection:中程度の注入
    • 依存関係をメソッドの引数として設定するため、柔軟性があるが、依存関係が初期化時に必ず提供されるわけではない。
    • 依存関係が後から設定されるため、初期化時に依存関係が存在しない場合がある。そのため、テストの設定が多少複雑になることがある。
/* Method Injection */
/* ソッドインジェクションでは、 */
/* 必要な依存オブジェクトをメソッドの引数として受け取る。 */

class A {
    private B b;

    // Bをメソッドで注入
    public void setB(B b) {
        this.b = b;
    }
}

class B {
    private C c;

    // Cをメソッドで注入
    public void setC(C c) {
        this.c = c;
    }
}

class C {
    // 何らかの処理
}

C c = new C();
B b = new B();
b.setC(c);
A a = new A();
a.setB(b);


  1. Interface Injection:中程度の注入
    • インターフェースに依存関係を注入するためのメソッドを定義することで、柔軟性があるが、設計が複雑になることがある。
    • 依存関係が明示的に示されるため、テストが可能だが、実装の煩雑さがリスクとなることがある。
/* Interface Injection */
/* インターフェースインジェクションは、 */
/* 依存関係を注入するためのメソッドを持つインターフェースを定義し、 */
/* そのインターフェースを実装することによって依存関係を提供する。 */
/* このアプローチはあまり一般的ではない。 */

interface Injectable {
    void inject(B b);
}

class A implements Injectable {
    private B b;

    @Override
    public void inject(B b) { // インターフェースメソッドを通じて注入
        this.b = b;
    }
}


  1. List Injection, Collection Injection:中程度の注入
    • 複数の依存関係を一度に注入できるため、簡潔で柔軟。依存関係が明確に示されるため、テストが比較的容易。
    • ただし、リストやコレクションが空の場合の処理や、依存関係の初期化の不確実性が考慮される必要がある。
/* List Injection, Collection Injection */
/* リストやコレクションを使用して、複数の依存関係を一度に注入する方法。 */
/* 特に、"同じ型の複数のBean" がある場合に便利。 */

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
class A {
    private final List<B> bList;

    @Autowired // コレクションインジェクション
    public A(List<B> bList) {
        this.bList = bList;
    }
}


  1. Property Injection:低い注入
    • 設定ファイルを通じて依存関係を注入するため、柔軟性が高いが、初期化が不確実になる可能性がある。
    • 依存関係が明示的ではなく、設定が適切に行われていない場合、プログラムの実行時にエラーが発生するリスクがある。
/* Property Injection */
/* プロパティインジェクションは、 */
/* 設定ファイルやプロパティファイルを通じて依存関係を注入する方法 */

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
class A {
    /* プロパティファイルから値を読み込むために、@Value アノテーションを使用。 */
    /* このアノテーションをクラスのフィールドに付与することで、 */
    /* 指定したプロパティの値を自動的に注入できる。 */
    @Value("${b.bean.name}") // プロパティファイルから値を注入
    private String bName;

    // bNameを使用するメソッド
}

プロパティインジェクションは、アプリケーションの設定や依存関係を外部のプロパティファイルから読み込み、オブジェクトに注入する方法。これにより、アプリケーションの設定を柔軟に変更できるようになる。Spring Bootでは、デフォルトで application.properties または application.yml という名前のファイルが設定ファイルとして使用される。このファイルにアプリケーションの設定を記述する。

# application.propertiesファイル例
# データベース接続設定
db.url=jdbc:mysql://localhost:3306/mydb
db.username=root
db.password=secret

# アプリケーションの設定
app.name=My Application
app.version=1.0.0

アプリケーションが起動すると、Spring Bootは application.properties ファイルを自動的に読み込み、@Value アノテーションで指定されたプロパティの値がクラスのフィールドに注入される。

Spring Bootでは、YAML形式の設定ファイルも使用できる。YAMLファイルの名前は application.yml で、以下のように記述する。

db:
  url: jdbc:mysql://localhost:3306/mydb
  username: root
  password: secret

app:
  name: My Application
  version: 1.0.0

Spring Bootでは、プロファイル機能を使用して異なる環境に応じたプロパティファイルを作成することができる(プロパティのオーバーライド)。例えば、開発環境用の application-dev.properties や本番環境用の application-prod.properties などを作成し、実行時に指定することで、環境ごとに異なる設定を簡単に管理することができる。


  1. Setter Injection:低い注入
    • 依存関係がセッターメソッドを通じて設定されるため、柔軟性が高いが、依存関係が不明瞭になる可能性がある。
    • 初期化時に依存関係が設定されていないと、メソッドを呼び出すときにエラーが発生するリスクがある。
    • 依存関係が明示的ではないため、テストが難しくなることがある。
/* Setter Injection */
/* セッターインジェクションでは、 */
/* 依存オブジェクトをセッターメソッドを通じて設定する */

class A {
    private B b;

    // Bをセッターで注入
    public void setB(B b) {
        this.b = b;
    }
}

class B {
    private C c;

    // Cをセッターで注入
    public void setC(C c) {
        this.c = c;
    }
}

class C {
    // 何らかの処理
}

C c = new C();
B b = new B();
b.setC(c);
A a = new A();
a.setB(b);

まとめ

  • 直行性(Loose Coupling)とは、クラスやモジュール間の依存関係を示す。直行性(Loose Coupling)が高い=クラスやモジュール間の依存関係が緩やかである。
  • 依存性注入(DI)とは、オブジェクトの依存関係を外部から注入することで、オブジェクトの生成や管理を行うデザインパターンのことを示す。
  • 依存性注入(DI)により、クラスは自分自身で依存するオブジェクトを生成せず、必要な依存関係を外部から受け取る。依存性注入(DI)により、クラス間の結合度を下げることができる。よって、依存性が緩やかになり、あるクラスの実装を変更しても、他のクラスに影響を与えることが少なくなる。これは、アプリケーションの保守性や拡張性を高める要因となる。

Discussion