Внедрение зависимостей (dependency injection): примеры

Пример без внедрения зависимости

В следующем примере Java класс Client содержит переменную Service, которая инициализируется конструктором Client. Клиент контролирует, какая реализация сервиса используется, и контролирует его построение. В этой ситуации говорят, что клиент имеет жесткую зависимость от ExampleService.

// Пример без внедрения зависимости
public class Client {
    // внутренняя ссылка на сервис, 
    // используемый этим клиентом
    private ExampleService service;

    // Конструктор
    Client() {
        // Указываем конкретную реализацию в конструкторе 
        // вместо использования внедрения зависимости
        service = new ExampleService();
    }

    // Метод в клиенте, который использует сервис
    public String greet() {
        return "Hello " + service.getName();
    }
}

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

Типы внедрения зависимостей

Существует как минимум три способа, которыми клиентский объект может получить ссылку на внешний модуль:

  • Внедрение в конструкторе: зависимости предоставляются через конструктор класса клиента.
  • Внедрение через сеттер: клиент предоставляет метод сеттер, который используется инжектором для внедрения зависимости.
  • Внедрение через интерфейс: интерфейс зависимости предоставляет метод инжектора, который внедрит зависимость в любой переданный ей клиент. Клиенты должны реализовать интерфейс, который предоставляет метод установки (сеттер), который принимает зависимость.

Другие типы внедрения зависимостей

В структурах DI (dependency injection) могут быть другие типы инъекций, помимо представленных выше.

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

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

Внедрение в конструкторе

Этот метод требует, чтобы клиент предоставил параметр в конструкторе для зависимости.

// Контруктор
Client(Service service) {
    // Сохраняем ссылку на переданный сервис 
    // внутри этого клиента
    this.service = service;
}

Внедрение через сеттер

Этот метод требует, чтобы клиент предоставил метод сеттер для зависимости.

// Метод сеттер
public void setService(Service service) {
    // Сохраняем ссылку на переданный сервис 
    // внутри этого клиента.
    this.service = service;
}

Внедрение через интерфейс

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

// Интерфейс сеттера сервиса.
public interface ServiceSetter {
    public void setService(Service service);
}

// Класс клиента
public class Client implements ServiceSetter {
    // Внутренняя ссылка на сервис, 
    // используемый этим клиентом.
    private Service service;

    // Установить сервис, 
    // который должен использовать этот клиент.
    @Override
    public void setService(Service service) {
        this.service = service;
    }
}

Сравнение внедрения в конструкторе

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

// Конструктор
Client(Service service, Service otherService) {
    if (service == null) {
        throw new InvalidParameterException("service must not be null");
    }
    if (otherService == null) {
        throw new InvalidParameterException("otherService must not be null");
    }

    // Сохраняем ссылки на сервисы внутри клиента
    this.service = service;
    this.otherService = otherService;
}

Сравнение внедрения через сеттер

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

// Установить сервис, 
// который будет использоваться этим клиентом
public void setService(Service service) {
    if (service == null) {
        throw new InvalidParameterException("service must not be null");
    }
    this.service = service;
}

// Установить другой сервис 
// для использования этим клиентом
public void setOtherService(Service otherService) {
    if (otherService == null) {
        throw new InvalidParameterException("otherService must not be null");
    }
    this.otherService = otherService;
}

Поскольку эти инъекции происходят независимо друг от друга, невозможно определить, когда инжектор завершил подключение клиента. Зависимость может быть оставлена нулевой просто из-за того, что инжектор не может вызвать свой установщик (сеттер). Это вынуждает проверять, что внедрение было выполнено с момента сборки клиента до каждого его использования.

// Установить сервис, 
// который будет использоваться этим клиентом
public void setService(Service service) {
    this.service = service;
}

// Установить другой сервис 
// для использования этим клиентом
public void setOtherService(Service otherService) {
    this.otherService = otherService;
}

// Проверяем ссылки на сервисы этого клиента
private void validateState() {
    if (service == null) {
        throw new IllegalStateException("service must not be null");
    }
    if (otherService == null) {
        throw new IllegalStateException("otherService must not be null");
    }
}

// Метод, который использует ссылки на сервисы
public void doSomething() {
    validateState();
    service.doYourThing();
    otherService.doYourThing();
}

Сравнение внедрения через интерфейс

Преимущество внедрения через интерфейс состоит в том, что зависимости могут полностью игнорировать своих клиентов, но все же могут получать ссылку на нового клиента и, используя его, отправлять ссылку на себя обратно клиенту. Таким образом, зависимости становятся инжекторами. Ключевым моментом является то, что метод инъекции (который может быть просто классическим методом установки) предоставляется через интерфейс.

Сборщик все еще необходим для представления клиента и его зависимостей. Сборщик возьмет ссылку на клиента, приведёт ее к интерфейсу установщика, который устанавливает эту зависимость, и передаст ее этому объекту зависимости, который обернется и передаст ссылку на себя обратно клиенту.

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

// Интерфейс сеттера сервиса.
public interface ServiceSetter {
    public void setService(Service service);
}

// Класс клиента
public class Client implements ServiceSetter {
    // Внутренняя ссылка на сервис, 
    // используемый этим клиентом.
    private Service service;

    // Установить сервис, 
    // который должен использовать этот клиент.
    @Override
    public void setService(Service service) {
        this.service = service;
    }
}

// Класс инжектора
public class ServiceInjector {
    Set clients;
    public void inject(ServiceSetter client) {
        clients.add(client);
        client.setService(new ServiceFoo());
    }
    public void switchToBar() {
        for (Client client : clients) {
            client.setService(new ServiceBar());
        }
    }
}

// Классы сервисов
public class ServiceFoo implements Service {}
public class ServiceBar implements Service {}

Примеры сборки

Ручная сборка в main является одним из способов реализации внедрения зависимостей.

public class Injector {
    public static void main(String[] args) {
        // Сначала строим зависимости
        Service service = new ExampleService();

        // Внедрение сервиса, в стиле конструктора
        Client client = new Client(service);

        // Используем объекты
        System.out.println(client.greet());
    }   
}

В приведенном выше примере граф объекта создается вручную, а затем вызывается в одной точке, чтобы начать его работу. Важно отметить, что этот инжектор не является чистым. Он использует один из объектов, которые он создает. Он имеет исключительно конструкторские отношения с ExampleService, но смешивает конструкцию и использование клиента. Это не должно быть общим. Это, однако, неизбежно. Точно так же, как объектно-ориентированному программному обеспечению для начала необходим необъектно-ориентированный статический метод, такой как main(), так и графу объектов с внедренной зависимостью требуется по крайней мере одна (предпочтительно только одна) точка входа для начала всего процесса.

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

Фреймворки, такие как Spring, могут создавать эти же объекты и соединять их вместе, прежде чем возвращать ссылку на клиента. Все упоминания о конкретном ExampleService можно перенести из кода в данные конфигурации.

import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Injector {
    public static void main(String[] args) {
        // -- Сборка объектов -- //
        BeanFactory beanfactory = new ClassPathXmlApplicationContext("Beans.xml");
        Client client = (Client) beanfactory.getBean("client");

        // -- Использование объектов -- //
        System.out.println(client.greet());
    }
}

Фреймворки, такие как Spring, позволяют выводить детали сборки в конфигурационные файлы. Этот код (выше) создает объекты и связывает их вместе в соответствии с Beans.xml (ниже). ExampleService все еще создается, хотя упоминается только ниже. Длинный и сложный объектный граф может быть определен таким образом, и единственным классом, упомянутым в коде, будет класс с методом точки входа, которым в этом случае является greet().

<?xml version="1.0" encoding="UTF-8"?>
 <beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
  http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <bean id="service" class="ExampleService">
    </bean>

    <bean id="client" class="Client">
        <constructor-arg value="service" />        
    </bean>
</beans>

В приведенном выше примере Клиент и Сервис не должны были подвергаться каким-либо изменениям, которые будут предоставлены Spring. Им разрешено оставаться простыми POJO. Это показывает, как Spring может связывать сервисы и клиенты, которые совершенно не знают о своем существовании. Этого нельзя сказать, если в классы добавлены Spring аннотации. Благодаря тому, что аннотации и вызовы, характерные для Spring, не распространяются среди многих классов, система остается слабо зависимой от Spring. Это может быть важно, если система намерена пережить Spring.

Выбор содержания POJO в чистоте не обходится без затрат. Вместо того, чтобы тратить усилия на разработку и обслуживание сложных файлов конфигурации, можно просто использовать аннотации, чтобы пометить классы и позволить Spring выполнить остальную часть работы. Разрешение зависимостей может быть простым, если они следуют соглашению, такому как сопоставление по типу или по имени. Это выбирает соглашение по конфигурации. Также можно утверждать, что при рефакторинге в другую платформу удаление аннотаций, специфичных для фреймыорка, было бы тривиальной частью задачи, и многие аннотации внедрения теперь стандартизированы.

import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Injector {
  public static void main(String[] args) {
    // Собираем объекты
    BeanFactory beanfactory = new AnnotationConfigApplicationContext(MyConfiguration.class);
    Client client = beanfactory.getBean(Client.class);

    // Используем объекты
    System.out.println(client.greet());
  }
}

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@ComponentScan
public class MyConfiguration {
    @Bean
    public Client client(ExampleService service) {
        return new Client(service);
    }
}

@Component
public class ExampleService {
    public String getName() {
        return "World!";
    }
}

Сравнение сборки

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

Перемещая все знания инжектора, чистый клиент, лишенный знаний о внешнем мире, остается позади. Однако любой объект, который использует другие объекты, может считаться клиентом. Объект, который содержит main, не является исключением. Этот основной объект не использует внедрение зависимостей. На самом деле он использует паттерн локатора сервиса. Этого нельзя избежать, потому что выбор реализаций сервиса должен быть где-то сделан.

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

Паттерн внедрения зависимостей

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

public static void main(String[] args) throws IOException {

    // Строительный код. 
    Greeter greeter = new Greeter(System.out); 
    // Здесь может быть много строк, 
    // которые соединяют много объектов
    
    // Код поведения.
    greeter.greet(); 
    // Это один вызов одного метода 
    // для одного объекта в графе объектов
}

class Greeter {
    public void greet() {
        this.out.println("Hello world!");
    }
    public Greeter(PrintStream out) {
        this.out = out;
    }
    private PrintStream out;
}


Читайте также:

Комментарии

Putri Adiratnaa написал(а)…
можливість позики, запропонована містером Бенджаміном, яка рятує мою сім’ю від фінансової неволі {247officedept@gmail.com} привіт усім! Бенджамін, коли нас вигнали з дому, коли я вже не міг оплачувати рахунки, після того, як мене обманули різні компанії в Інтернеті та відмовили у позиці у моєму банку та іншій кредитній спілці, яку я відвідав. моїх дітей взяли в прийомні сім'ї, я був на вулиці один. день, коли я ганебно зайшов до старого шкільного товариша, який познайомив мене з маргариткою Морін. спочатку я сказав їй, що більше не готовий ризикувати запитом позики в Інтернеті, але вона запевнила мене, що отримаю свою позику від них. задумавшись, через свою безпритульність мені довелося взяти судовий розгляд та подати заявку на позику, на щастя для мене, я отримав позику у розмірі 80 000,00 доларів від пана Бенджаміна. Я щасливий, що ризикнув і подав заявку на позику. мої діти були повернені мені, і тепер я маю дім і власний бізнес. вся подяка та подяка йде на допомогу містеру Бенджаміну за те, що він дав мені сенс життя, коли я втратив будь-яку надію. якщо ви зараз шукаєте допомоги в позиці, ви можете зв’язатися з ними за адресою: {247officedept@gmail.com whatsapp + 1-989-394-3740.

Популярные сообщения из этого блога

Язык поисковых запросов в Graylog

Хэш-таблица: разрешение коллизий

Нормальные формы, пример нормализации в базе данных