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

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

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

Внедрение зависимостей (DI) может быть очень сложной темой для понимания, поскольку, похоже, происходит много “волшебства”. Обычно это включает в себя кучу аннотаций, разбросанных повсюду, с объектами, появляющимися, казалось бы, из ниоткуда. Я, конечно, знаю, что мне потребовалось много времени, чтобы по-настоящему понять эту концепцию. Если вам когда-нибудь будет трудно понять, что Spring и Java EE делают за кулисами (и почему!), Читайте дальше!

В этом руководстве мы собираемся создать очень простой, но полностью функциональный контейнер для внедрения зависимостей с нуля на Java. Вот несколько правил, чтобы все было управляемо:

  • Абсолютно никакие библиотеки не разрешены, за исключением самого JDK.
  • Никаких заранее подготовленных дампов кода. Мы пройдем через все и порассуждаем об этом.
  • Никаких наворотов и свистков, только основы.
  • Производительность не имеет значения, никаких оптимизаций.
  • Исполняемый файл main() методы на каждом шаге.

В отличие от других статей, я не буду заранее объяснять, что такое DI или почему он полезен. Вместо этого мы начнем с простого примера и позволим ему “развиваться”.

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

Мы начнем с очень простого примера. Нам нужны два класса, давайте назовем их исправными и Обслуживание , и ServiceA потребности ServiceB выполнять свою работу. Что ж, вы могли бы реализовать это с помощью static

public class ServiceA {

    public static String jobA(){
        return "jobA(" + ServiceB.jobB() + ")";
    }

}
public class ServiceB {

    public static String jobB(){
        return "jobB()";
    }

}
public class Main {

    public static void main(String[] args) {
        System.out.println(ServiceA.jobA());
    }

}

Если мы запустим этот код, он напечатает:

jobA(jobB())

Круто! Так зачем вообще беспокоиться о чем-то большем, чем это? Хорошо…

  • Код очень беден с точки зрения принципов OO. ServiceA и ServiceB должны, по крайней мере, быть объектами .
  • Код тесно связан, и его очень трудно тестировать изолированно.
  • У нас нет абсолютно никаких шансов заменить ни Сервис, ни Сервис другой реализацией. Представьте, что один из них выставляет счета по кредитным картам; вы не хотите, чтобы это действительно произошло в вашем наборе тестов.

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

Основная проблема, которую мы выявили на этапе 0, заключается в том, что существуют только статические методы, что приводит к чрезвычайно тесной связи. Мы хотели бы, чтобы наши сервисы были объектами, взаимодействующими друг с другом, чтобы мы могли заменять их по мере необходимости. Теперь возникает вопрос: как Служба узнает с какой Службой разговаривать? Самая основная идея состоит в том, чтобы просто передать наш экземпляр Service конструктору ServiceA:

public class ServiceA {

    private ServiceB serviceB;

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

    public String jobA(){
        return "jobA(" + this.serviceB.jobB() + ")";
    }

}

Служба B не сильно изменилась:

public class ServiceB {

    public String jobB() {
        return "jobB()";
    }

}

….. и Main теперь необходимо собрать объекты, прежде чем он сможет вызвать метод |/jobA :

   public static void main(String[] args) {
        ServiceB serviceB = new ServiceB();
        ServiceA serviceA = new ServiceA(serviceB);

        System.out.println(serviceA.jobA());
    }

Здесь нет ничего особенного, верно? Мы, безусловно, кое-что улучшили:

  • Теперь мы можем заменить реализацию Service , которая используется Services , предоставив другой объект, возможно, даже другого подкласса.
  • Мы можем протестировать обе службы изолированно с помощью надлежащего тестового макета для служб.

Так все круто? Не совсем:

  • Создавать макеты сложно, так как нам требуется class .
  • Было бы намного приятнее, если бы вместо этого мы потребовали interfaces . Кроме того, это еще больше уменьшило бы сцепление.

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

Итак, давайте использовать один интерфейс для каждого из наших сервисов. Они настолько просты, насколько это возможно (я переименовал фактические классы в Service impl и ServiceBImpl

public interface ServiceA {

    public String jobA();

}
public interface ServiceB {

    public String jobB();

}

Теперь в Serviceimpl , мы действительно можем использовать интерфейс:

public class ServiceAImpl implements ServiceA {

    private final ServiceB serviceB;

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

    // jobA() is the same as before
}

Это также делает наш метод main() немного приятнее:

  public static void main(String[] args) {
        ServiceB serviceB = new ServiceBImpl();
        ServiceA serviceA = new ServiceAImpl(serviceB);
        System.out.println(serviceA.jobA());
    }

Это было бы вполне приемлемо для простых вариантов использования с точки зрения OO. Если тебе это сойдет с рук, во что бы то ни стало остановись здесь. Однако по мере того, как ваш проект становится все больше…

  • будет становиться все сложнее и сложнее создавать сеть служб внутри вашего main() метода.
  • вы столкнетесь с циклами в зависимостях вашего сервиса, которые не могут быть разрешены с помощью конструкторов, как показано в нашем примере.

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

Давайте предположим, что не только Service нуждается в ServiceB , но и наоборот – у нас есть цикл. Конечно, мы все равно можем объявить параметр в конструкторе * Impl классы, например так:

 // constructor examples
 public ServiceAImpl(ServiceB serviceB) { ... }
 public ServiceBImpl(ServiceA serviceA) { ... }

… но это не принесет нам никакой пользы: мы не сможем создать фактический экземпляр любого из двух классов. Чтобы создать экземпляр Service impl , , сначала нам потребуется экземпляр ServiceB , и для создания экземпляра ServiceBImpl

Примечание: на самом деле есть выход из этого с помощью прокси . Однако здесь мы не собираемся идти по этому пути.

Так что же нам делать вместо этого? Что ж, поскольку мы имеем дело с циклическими зависимостями, нам нужна возможность сначала создать сервисы а затем соедините их вместе. Мы делаем это с сеттерами:

public class ServiceAImpl implements ServiceA {

    private ServiceB serviceB;

    // no constructor anymore here!

    @Override // <- added getter to ServiceA interface
    public ServiceB getServiceB() { return serviceB; }

    @Override // <- added setter to ServiceA interface
    public void setServiceB(final ServiceB serviceB) { this.serviceB = serviceB; }

    // jobA() same as before
}

Наш метод main() выглядит следующим образом:

    public static void main(String[] args) {
        // create instances
        ServiceB serviceB = new ServiceBImpl();
        ServiceA serviceA = new ServiceAImpl();

        // wire them together
        serviceA.setServiceB(serviceB);
        serviceB.setServiceA(serviceA);

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

Вы видите закономерность? Сначала создайте объекты, затем соедините их, чтобы сформировать график обслуживания (т.е. сеть обслуживания).

Так почему бы не остановиться на этом?

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

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

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