なんとなく使っているDIのありがたさを理解する

## はじめに
筆者はSIerでエンジニアをしております。やりたいことよりも、案件をベースに、その案件に合わせた技術スタックを修得しています。

今回は、やっと腰を据えてSpringBootを扱いました。あらためてDIのありがたさについて学んだことを記載します。

## 目次
- [なんとなく使っているDIのありがたさを理解する](#なんとなく使っているdiのありがたさを理解する)
  - [目次](#目次)
  - [はじめに](#はじめに)
  - [前提](#前提)
  - [DIとは](#diとは)
  - [DIについて理解を深める](#diについて理解を深める)
    - [前提](#前提-1)
    - [DIを使わない場合](#diを使わない場合)
    - [DIを使う場合](#diを使う場合)
      - [テストがしやすくなる例](#テストがしやすくなる例)
      - [新機能追加時の変更が最小限で済む例](#新機能追加時の変更が最小限で済む例)
      - [実際はそんなに甘くない例](#実際はそんなに甘くない例)
    - [ここで、DIコンテナですよ](#ここでdiコンテナですよ)
  - [まとめ](#まとめ)

## 前提
以下の技術スタックを用います。
- java(たぶんバージョンに影響しない概念です。)
- SpringBoot(たぶんバージョンに影響しない概念です。)

## DIとは
Dependency Injection。直訳で依存性の注入。クラスAからクラスBに直接依存するのではなく、外側から実体への依存を注入できるようにする技術。こんなこと言ってもわからないので、実例と、使わない時の問題、使ったら嬉しくなることを見ていきましょう。

## DIについて理解を深める
### 前提
- mainからクラスAを呼び出し、クラスAの中ではクラスBを使用します。

### DIを使わない場合
愚直に実装するといかのようになります。まあ、悪くはないですね。

```java
// Main.java
public class Main {
    public static void main(String[] args) {
        ClassA classA = new ClassA();
        classA.doSomething();
    }
}

// ClassA.java
public class ClassA {
    private ClassB classB;

    public ClassA() {
        // クラスAの中で直接ClassBのインスタンスを生成している
        this.classB = new ClassB();
    }

    public void doSomething() {
        // ClassBの処理を呼び出す
        String result = classB.process();
        System.out.println("ClassA: " + result);
    }
}

// ClassB.java
public class ClassB {
    public String process() {
        return "Processing in ClassB";
    }
}
```

一見、「いいんじゃね」と思うのですが、以下のような問題があります。

- クラスBが外部APIなどを呼び出している場合にテストが困難
- クラスBのインタフェースが変更される場合に、クラスAの変更も必要になる 

実装としては間違ってはいないのですが、拡張性や保守性に問題が生じる可能性があります。

ということで、これをDIを実装してみましょう。

### DIを使う場合
以下のように、クラスAを生成する際に、クラスBも生成して注入します。これが、Aからみると、上位から「注入」されていることになります。

```java
// ProcessorB.java (インターフェース)
public interface ProcessorB {
    String process();
}

// ClassB.java (実装クラス)
public class ClassB implements ProcessorB {
    @Override
    public String process() {
        return "Processing in ClassB";
    }
}

// ClassA.java
public class ClassA {
    private final ProcessorB processor;  // インターフェースを使用

    // コンストラクタインジェクション
    public ClassA(ProcessorB processor) {
        this.processor = processor;
    }

    public void doSomething() {
        String result = processor.process();
        System.out.println("ClassA: " + result);
    }
}

// Main.java
public class Main {
    public static void main(String[] args) {
        ProcessorB processor = new ClassB();
        ClassA classA = new ClassA(processor);  // 依存性を外部から注入
        classA.doSomething();
    }
}
```

で、、、、なにが嬉しいのか。それは以下のようなパターンで、DIの恩恵が受けられます。
- テストがしやすくなる
- 新機能追加時の変更が最小限で済む
- クラスBはインタフェースに従った実装をする

#### テストがしやすくなる例
テスト時には、外部APIを呼び出さないモック実装を注入できます。
```java
// テスト用のモック実装
public class MockClassB implements ProcessorB {
    @Override
    public String process() {
        return "テスト用の固定値";
    }
}

// テストコード
public class ClassATest {
    @Test
    public void testDoSomething() {
        // テスト用のモックを注入
        ProcessorB mockProcessor = new MockClassB();
        ClassA classA = new ClassA(mockProcessor);
        
        // テストの実行
        classA.doSomething();  // -> "ClassA: テスト用の固定値" と出力される
    }
}
```

#### 新機能追加時の変更が最小限で済む例
ログ出力機能を追加したい場合でも、既存コードを変更せずに対応できます。

```java
// ログ出力機能を追加した実装
public class LoggingClassB implements ProcessorB {
    private final ProcessorB original;
    
    public LoggingClassB(ProcessorB original) {
        this.original = original;
    }
    
    @Override
    public String process() {
        System.out.println("処理開始");
        String result = original.process();
        System.out.println("処理終了");
        return result;
    }
}

// 使用例
public class Main {
    public static void main(String[] args) {
        ProcessorB original = new ClassB();
        ProcessorB withLogging = new LoggingClassB(original);
        
        ClassA classA = new ClassA(withLogging);
        classA.doSomething();
    }
}
```

ちょっとだけでも、DIの嬉しさが伝わりましたでしょうか。
でも、、、単純な例だと嬉しいのですが、クラスの階層はもっと深いですよね。実際にはこんなことになります。

#### 実際はそんなに甘くない例
例えば、以下のような3階層の依存関係があるとします。
- ClassA → ServiceB → RepositoryC
そうすると、以下のように全部の依存関係の構築が必要になります。

```java
// Main.java
public class Main {
    public static void main(String[] args) {
        // すべての依存関係を手動で構築する必要がある
        RepositoryC repositoryC = new RepositoryC();
        ServiceB serviceB = new ServiceB(repositoryC);
        ClassA classA = new ClassA(serviceB);
        
        classA.doSomething("データ");
    }
}

// ClassA.java
public class ClassA {
    private final ServiceB serviceB;

    public ClassA(ServiceB serviceB) {
        this.serviceB = serviceB;
    }

    public void doSomething(String data) {
        String result = serviceB.process(data);
        System.out.println("ClassA: " + result);
    }
}

// ServiceB.java
public class ServiceB {
    private final RepositoryC repositoryC;

    public ServiceB(RepositoryC repositoryC) {
        this.repositoryC = repositoryC;
    }

    public String process(String data) {
        // データを加工してから保存
        String processed = data + "_processed";
        return repositoryC.save(processed);
    }
}

// RepositoryC.java
public class RepositoryC {
    public String save(String data) {
        // 実際にはDBに保存する処理
        return "Saved: " + data;
    }
}
```

うーん、めんどくさいですね。

### ここで、DIコンテナですよ
これまでは手作業でDIを実装してきました。階層がう深くなると手作業で管理は効率悪いですね。それも仕組みに任せましょう。ここではSpringBootのDIコンテナ使用して、ありがたみを感じましょう。

同じ構成をSpringBootで実装すると、以下のようになります。DataControllerというAPIを実装することとします。

```java
// ClassA.java
@Component  // コンポーネントとして登録
public class ClassA {
    private final ServiceB serviceB;

    @Autowired  // DIコンテナによる依存性の注入
    public ClassA(ServiceB serviceB) {
        this.serviceB = serviceB;
    }

    public void doSomething(String data) {
        String result = serviceB.process(data);
        System.out.println("ClassA: " + result);
    }
}

// ServiceB.java
@Service  // サービスとして登録
public class ServiceB {
    private final RepositoryC repositoryC;

    @Autowired  // DIコンテナによる依存性の注入
    public ServiceB(RepositoryC repositoryC) {
        this.repositoryC = repositoryC;
    }

    public String process(String data) {
        String processed = data + "_processed";
        return repositoryC.save(processed);
    }
}

// RepositoryC.java
@Repository  // リポジトリとして登録
public class RepositoryC {
    public String save(String data) {
        return "Saved: " + data;
    }
}

// Controller層を追加
@RestController
@RequestMapping("/api")
public class DataController {
    private final ClassA classA;

    @Autowired
    public DataController(ClassA classA) {
        this.classA = classA;
    }

    @PostMapping("/process")
    public ResponseEntity<String> processData(@RequestBody DataRequest request) {
        String result = classA.doSomething(request.getData());
        return ResponseEntity.ok(result);
    }
}
```

さて、何か簡素になりました。それぞれのクラスにアノテーションがつきましたが、コンストラクタの引数でのやり取りは減りました。

SpringBootの環境下で、ルールに従ってアノテーションを付与することによって、DIコンテナが使用できて、
- 自動で依存を解決
- 手動での注入が不要
など、手動DI以上の恩恵が受けられます。

これでクラス間の依存関係を管理をSpringBootにお任せすることができます。

## まとめ

DIを使わないコードでも、動作はします。でも、テストのしづらさや、機能追加時の改修範囲の広がりなど、いくつかの問題が出てきます。

そこでDIを使うと
- テストが楽になる（モック実装を注入できる）
- 新機能追加が楽になる（既存コードの変更が最小限で済む）

ただし、手動でDIを実装すると、クラスの階層が深くなるにつれて大変になってきます。たとえば3階層の依存関係だと、すべての依存関係を手動で構築する必要があって、なかなかめんどくさい。

そこで、SpringBootなどのDIコンテナです。アノテーションを付けるだけで
- 自動で依存関係を解決してくれる
- インスタンス生成の順番とかを気にしなくて良くなる

なんとなく使っていたDIですが、ちょっとだけでもそのありがたさが伝わりましたでしょうか。

生のJavaからSpringBootにステップアップしたら避けては通れない概念です。
最初？の壁になりそうなDIの概念について、少しでも理解が進めば嬉しいです。