Рубрики
Без рубрики

Понимание внедрения зависимостей путем написания контейнера DI – с нуля! (Часть 2)

Внедрение зависимостей – сложная тема для понимания. Давайте разберем его на простые части, написав базовый, но полностью функциональный DI-контейнер с нуля!. Помеченный как java, внедрение зависимостей, spring, jee.

Это вторая часть моей серии “DI с нуля”. В предыдущей статье мы обсудили наш базовый пример и проблемы, связанные с “ручным” подходом. Теперь мы хотим автоматизировать подключение нашей сервисной сети.

Найдите исходный код этого раздела на github

Давайте сосредоточимся на методе main() и попытаемся найти более автоматический способ создания сервисной сети. Конечно, забыть вызвать сеттер здесь – реальная угроза, и компилятор даже не сможет предупредить вас об этом. Но наши сервисы структурно отличаются, и нет единого способа доступа к ним … или есть? Java Reflection спешит на помощь!

Все, что мы, по сути, хотим предоставить нашей установке, – это список наших классов обслуживания. Оттуда настройка должна построить и подключить сервисную сеть. В частности, наш метод main() действительно заинтересован только в Сервисах , для этого даже не нужно Услуга . Давайте перепишем это так:

    public static void main(String[] args) throws Exception {
        Set> serviceClasses = new HashSet<>();
        serviceClasses.add(ServiceAImpl.class);
        serviceClasses.add(ServiceBImpl.class);

        ServiceA serviceA = createServiceA(serviceClasses);

        // call business logic
        System.out.println(serviceA.jobA());
    }

Но как мы можем реализовать “волшебный” метод createServiceA ? Оказывается, это не так сложно…

    private static ServiceA createServiceA(Set> serviceClasses) throws Exception{
        // step 1: create an instance of each service class
        Set serviceInstances = new HashSet<>();
        for(Class serviceClass : serviceClasses){
            Constructor constructor = serviceClass.getConstructor();
            constructor.setAccessible(true);
            serviceInstances.add(constructor.newInstance());
        }
        // step 2: wire them together
        for(Object serviceInstance : serviceInstances){
            for(Field field : serviceInstance.getClass().getDeclaredFields()){
                Class fieldType = field.getType();
                field.setAccessible(true);
                // find a suitable matching service instance
                for(Object matchPartner : serviceInstances){
                    if(fieldType.isInstance(matchPartner)){
                        field.set(serviceInstance, matchPartner);
                    }
                }
            }
        }
        // step 3: from all our service instances, find ServiceA
        for(Object serviceInstance : serviceInstances){
            if(serviceInstance instanceof ServiceA){
                return (ServiceA)serviceInstance;
            }
        }
        // we didn't find the requested service instance
        return null;
    }

Давайте разберем это по полочкам. В Шаге 1 мы перебираем наши классы, и для каждого класса мы пытаемся получить конструктор по умолчанию (т.е. конструктор без аргументов). Поскольку ни ServiceAImpl , ни ServiceBImpl не указывают никакого конструктора (мы удалили их при введении getters/setters), компилятор Java предоставляет общедоступный конструктор по умолчанию – так что это будет работать нормально. Затем мы делаем этот конструктор доступным . Это просто защитное программирование, чтобы убедиться, что частные конструкторы тоже будут работать. Наконец, мы вызываем newInstance() в конструкторе, чтобы создать экземпляр класса, и добавить это к нашему набору экземпляров.

В Шаге 2 мы хотим соединить наши отдельные экземпляры сервиса. Для этого мы рассматриваем каждый объект сервиса один за другим. Мы извлекаем его класс Java через getClass() и запрашиваем у этого класса все его объявленные поля ( ( объявленный означает, что частные поля тоже будут возвращены). Так же, как и для конструктора, мы убеждаемся, что поле доступно, а затем проверяем Введите поля. Это предоставит нам класс обслуживания, который нам нужно ввести в поле. Все, что осталось сделать, это найти подходящего подходящего партнера , объект, который имеет тип, указанный в поле. Как только мы его найдем, мы вызываем field.set(…) и назначьте партнера по матчу на поле. Обратите внимание, что первый параметр метода field.set(…)

В Шаге 3 сеть уже завершена; все, что осталось сделать, это найти экземпляр Service A . Мы можем просто просмотреть экземпляры или и проверить, нашли ли мы правильный, используя instanceof serviceable .

Это может показаться немного пугающим, так что, возможно, попробуйте прочитать это еще раз. Кроме того, вы можете освежить свои знания основ отражения Java, если что-то из этого покажется вам странным.

Так что же мы получили?

  • Наши услуги подключаются автоматически.
  • Мы больше не можем забывать вызывать сеттера (на самом деле они нам больше не нужны).
  • Наше приложение завершится ошибкой при запуске если подключение выходит из строя, то не во время бизнес-логики.

Основная проблема, которую нам нужно решить в следующий раз, заключается в том, что мы не хотим повторять всю эту процедуру каждый раз, когда хотим получить доступ к сервису; мы хотим иметь возможность получить доступ к каждой службе в сети, а не только к одной.

Найдите исходный код этого раздела на github

Объект, который отвечает за удержание сервисной сети, называется Контейнер для внедрения зависимостей или (в терминах Spring) Контекст приложения . Я собираюсь использовать терминологию “контекста”, но на самом деле эти термины являются синонимами. Основная задача контекста – предоставить метод getServiceInstance(...) , который принимает класс сервиса в качестве параметра и возвращает (готовый и подключенный) экземпляр службы. Итак, поехали:

public class DIContext {

    private final Set serviceInstances = new HashSet<>();

    public DIContext(Collection> serviceClasses) throws Exception {
        // create an instance of each service class
        for(Class serviceClass : serviceClasses){
            Constructor constructor = serviceClass.getConstructor();
            constructor.setAccessible(true);
            Object serviceInstance = constructor.newInstance();
            this.serviceInstances.add(serviceInstance);
        }
        // wire them together
        for(Object serviceInstance : this.serviceInstances){
            for(Field field : serviceInstance.getClass().getDeclaredFields()){
                Class fieldType = field.getType();
                field.setAccessible(true);
                // find a suitable matching service instance
                for(Object matchPartner : this.serviceInstances){
                    if(fieldType.isInstance(matchPartner)){
                        field.set(serviceInstance, matchPartner);
                    }
                }
            }
        }
    }

    @SuppressWarnings("unchecked")
    public  T getServiceInstance(Class serviceClass){
        for(Object serviceInstance : this.serviceInstances){
            if(serviceClass.isInstance(serviceInstance)){
                return (T)serviceInstance;
            }
        }
        return null;
    }

}

Как вы можете видеть, код не сильно изменился по сравнению с предыдущим шагом, за исключением того, что теперь у нас есть объект для инкапсуляции контекста ( DIContext ). Внутренне он управляет набором экземпляров службы , который создается точно так же, как и раньше, из коллекции классов служб. Приведенный выше Шаг 3 перешел в свой собственный метод getServiceInstance , который принимает класс для извлечения в качестве параметра. Поскольку мы не можем использовать instanceof больше (для этого требуется жестко запрограммированный класс, а не значение динамической переменной), мы должны вернуться к ServiceClass.isInstance(…)

Мы можем использовать этот класс в нашем новом main() :

    public static void main(String[] args) throws Exception {
        DIContext context = createContext();
        doBusinessLogic(context);
    }

    private static DIContext createContext() throws Exception {
        Set> serviceClasses = new HashSet<>();
        serviceClasses.add(ServiceAImpl.class);
        serviceClasses.add(ServiceBImpl.class);
        return new DIContext(serviceClasses);
    }

    private static void doBusinessLogic(DIContext context){
        ServiceA serviceA = context.getServiceInstance(ServiceA.class);
        ServiceB serviceB = context.getServiceInstance(ServiceB.class);
        System.out.println(serviceA.jobA());
        System.out.println(serviceB.jobB());
    }

Как вы можете видеть, теперь мы можем легко извлекать полные экземпляры службы из контекста, вызывая getServiceInstance так часто, как нам нужно, с разными классами ввода. Также обратите внимание, что сами службы могут получать доступ друг к другу, просто объявив поле соответствующего типа – им даже не нужно знать о Dircontext объект.

Однако некоторые проблемы все еще существуют. Например, что, если мы хотим иметь поле в наших службах, которое не ссылается на другую службу (скажем, поле int )? Нам нужен способ указать нашему алгоритму , какие поля мы хотим, чтобы он установил, а какие оставить в покое.

Найдите исходный код этого раздела на github

Итак, как мы можем указать нашему алгоритму, какие поля ему нужно назначить? Мы могли бы ввести какую-нибудь причудливую схему именования и проанализировать field.getName() , но это очень подверженное ошибкам решение. Вместо этого мы будем использовать Аннотацию :

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {

}

@Target сообщает компилятору, для каких элементов мы можем использовать эту аннотацию – мы хотим, чтобы она была применима к полям. С помощью @Retention мы даем указание компилятору сохранить эту аннотацию до выполнения, а не отбрасывать ее во время компиляции.

Давайте прокомментируем наши поля:

public class ServiceAImpl implements ServiceA {

    @Inject
    private ServiceB serviceB;

    // rest is the same as before

}
public class ServiceBImpl implements ServiceB {

    @Inject
    private ServiceA serviceA;

    // rest is the same as before

}

Аннотация сама по себе и сама по себе не делает ничего . Нам нужно активно читать аннотацию. Итак, давайте сделаем это в конструкторе нашего Dircontext :

       // wire them together
       for(Object serviceInstance : this.serviceInstances){
           for(Field field : serviceInstance.getClass().getDeclaredFields()){
               // check that the field is annotated
               if(!field.isAnnotationPresent(Inject.class)){
                   // this field is none of our business
                   continue;
               }
               // rest is the same as before

Снова запустите метод main() ; он должен работать так же, как и раньше. Однако теперь вы можете свободно добавлять дополнительные поля в свои сервисы, и алгоритм подключения не нарушится.

До сих пор мы создали контейнер DI, который является очень простым, но функциональным. Он полагается на то, что мы предоставляем ему набор классов обслуживания. В следующей части мы обсудим, как мы можем на самом деле обнаружить наши классы обслуживания.

Оригинал: “https://dev.to/martinhaeusler/understanding-dependency-injection-by-writing-a-di-container-from-scratch-part-2-2np6”