LoginSignup
0
0

DIコンテナを自作する

Posted at

DIとは?

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

DIコンテナとは?

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

作ったもの

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

ソースコード解説

コアの部分はConstructorInjectionContainer.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から呼び出す

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でコンストラクタを登録

    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がやる。

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

といってもgetInstancecreateInstanceを読んでいるだけで、ロジックのコアはcreateInstanceの中にある。

    @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()を返すことにしている。

var constructor = constructorMap.get(clazz.getName()) != null ? constructorMap.get(clazz.getName()) : clazz.getDeclaredConstructor();

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

Stream.of(constructor.getParameterTypes())

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

.map(parameterType -> getInstance(parameterType))

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

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0