DIコンテナを自作する

# DIとは？
- Dependency Injection - 依存性注入のこと
- クラス間の依存関係を外部から注入することで、開発者側でコンストラクタによるインスタンス生成が不要になる
    - `new Hoge()`をせずともHogeクラスのインスタンスを利用できる
- Spring Bootだと以下3パターンがある
    - コンストラクタインジェクション
    - フィールドインジェクション
    - セッターインジェクション
- ベストプラクティスはコンストラクタインジェクション

# DIコンテナとは？
- DIを実施するコンテナのこと
- プロジェクト内のクラス間の依存関係を全て把握し、よしなにDIしてくれて、かつその処理を開発者の見えないところでひっそりやってくれる
- 汚れ仕事を全て引き受けてくれる黒子みたいな存在

# 作ったもの
https://github.com/tttol/MicroDI

自分がDIコンテナのことを理解するために作ったものなので、**作業記録みたいなものと思ってもらえれば。**
実際のアプリ開発で利用できるクオリティには程遠いので、Spring Bootが用意してるDIコンテナを使いましょう。

# ソースコード解説
コアの部分はConstructorInjectionContainer.java↓
```java:ConstructorInjectionContainer.java
package di;

import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ConstructorInjectionContainer {
    static Map<String, Constructor<?>> constructorMap = new HashMap<>();

    public static void register(Class<?> clazz) {
        Stream.of(clazz.getConstructors())
            .filter(constructor -> constructor.getParameterCount() > 0) //デフォルトコンストラクタを除外
            .forEach(nullableConstructor -> {
                constructorMap.put(clazz.getName(), nullableConstructor);
            });
    }

    public static Object getInstance(Class<?> clazz) {
        try {
            return createInstance(clazz);
        } catch (Exception e) {
            log.warn("Failed to getInstance. message={}, cause={}", e.getMessage(), e.getCause());
            return null;
        }
    }

    @SuppressWarnings("unchecked")
    private static <T> T createInstance(Class<T> clazz) {
        try {
            var constructor = constructorMap.get(clazz.getName()) != null ? constructorMap.get(clazz.getName()) : clazz.getDeclaredConstructor();
            return (T) constructor
                .newInstance(Stream.of(constructor.getParameterTypes())
                    .map(parameterType -> getInstance(parameterType))
                    .toArray()
                );
        } catch (Exception e) {
            log.warn("Failed to createInstance", e);
            return null;
        }
    }
}
```

動作確認時はこれをMainから呼び出す

```java
import di.ConstructorInjectionContainer;
import di.FieldInjectionContainer;
import module.constructor.ClassD;
import module.constructor.ClassE;
import module.constructor.ClassF;
import module.field.ClassA;

public class Main {
    public static void main(String[] args) {
        ConstructorInjectionContainer.register(ClassD.class);
        ConstructorInjectionContainer.register(ClassE.class);
        ConstructorInjectionContainer.register(ClassF.class);

        var classD = (ClassD) ConstructorInjectionContainer.getInstance(ClassD.class);
        classD.print();
    }
}
```

## ①registerでコンストラクタを登録
```java
    public static void register(Class<?> clazz) {
        Stream.of(clazz.getConstructors())
            .filter(constructor -> constructor.getParameterCount() > 0) //デフォルトコンストラクタを除外
            .forEach(nullableConstructor -> {
                constructorMap.put(clazz.getName(), nullableConstructor);
            });
    }
```
まずは`Map<String, Constructor<?>> constructorMap`というMapに{クラス名, コンストラクタ}の組を登録していく。
あるクラスのインスタンス生成にどのコンストラクタが必要かをMapで管理する。

## ②getInstance -> createInstanceをコールして、インスタンスを取得する
インスタンス取得は`getInstance`がやる。
```java
    public static Object getInstance(Class<?> clazz) {
        try {
            return createInstance(clazz);
        } catch (Exception e) {
            log.warn("Failed to getInstance.", e);
            return null;
        }
    }
```

といっても`getInstance`は`createInstance`を読んでいるだけで、ロジックのコアは`createInstance`の中にある。
```java
    @SuppressWarnings("unchecked")
    private static <T> T createInstance(Class<T> clazz) {
        try {
            var constructor = constructorMap.get(clazz.getName()) != null ? constructorMap.get(clazz.getName()) : clazz.getDeclaredConstructor();
            return (T) constructor
                .newInstance(Stream.of(constructor.getParameterTypes())
                    .map(parameterType -> getInstance(parameterType))
                    .toArray()
                );
        } catch (Exception e) {
            log.warn("Failed to createInstance", e);
            return null;
        }
    }
```

まずはインスタンス生成したいクラスが引数の`clazz`に入っているので、これをkeyにして`constructorMap`からコンストラクタを特定する。
Mapにコンストラクタがなければ、`getDeclaredConstructor()`を返すことにしている。
```java
var constructor = constructorMap.get(clazz.getName()) != null ? constructorMap.get(clazz.getName()) : clazz.getDeclaredConstructor();
```

得られたコンストラクタから`newInstance`メソッドをコールして、インスタンスを生成する。
ただし、コンストラクタの引数はクラスによって異なるので柔軟に対応しないといけない。
とりあえず`constructor.getParameterTypes()`で引数のリストを取ってきてStreamにする。
```java
Stream.of(constructor.getParameterTypes())
```

得られたparameterTypeから型情報（java.lang.Class）を取得して`getInstance`に再帰的に渡す。
ここが一番のミソ。
```java
.map(parameterType -> getInstance(parameterType))
```

コンストラクタ引数の数だけ`getInstance`が呼ばれ、最初にメソッドコールされたときの`clazz`に依存するクラス達のインスタンスが芋づる式に取得できる。


# 参考
https://nowokay.hatenablog.com/entry/20160406/1459918560

https://github.com/kishida/tinydi

https://speakerdeck.com/ryounasso/jian-yi-di-kontenawozuo-tute-di-kontenawozhi-ru

https://github.com/ryounasso/Simple-DI

https://fukabori.fm/episode/48