«1. Обзор

Принцип обращения зависимостей (DIP) является частью набора принципов объектно-ориентированного программирования, широко известного как SOLID.

По сути, DIP — это простая, но мощная парадигма программирования, которую мы можем использовать для реализации хорошо структурированных, сильно развязанных и повторно используемых программных компонентов.

В этом руководстве мы рассмотрим различные подходы к реализации DIP — один в Java 8 и один в Java 11 с использованием JPMS (Java Platform Module System).

2. Внедрение зависимостей и инверсия управления не являются реализациями DIP

Прежде всего, давайте сделаем фундаментальное различие, чтобы правильно понять основы: DIP не является ни внедрением зависимостей (DI), ни инверсией управления (IoC). Тем не менее, все они отлично работают вместе.

Проще говоря, DI заключается в том, чтобы программные компоненты явно объявляли свои зависимости или соавторов через свои API, вместо того, чтобы приобретать их сами по себе.

Без внедрения зависимостей программные компоненты тесно связаны друг с другом. Следовательно, их трудно повторно использовать, заменять, имитировать и тестировать, что приводит к жесткой конструкции.

При использовании внедрения зависимостей ответственность за предоставление зависимостей компонентов и связывание графов объектов переносится с компонентов на лежащую в их основе структуру внедрения. С этой точки зрения DI — это просто способ достижения IoC.

С другой стороны, IoC — это шаблон, в котором управление потоком приложения меняется на противоположное. При использовании традиционных методологий программирования наш пользовательский код контролирует поток приложения. И наоборот, с IoC управление передается внешней структуре или контейнеру.

Фреймворк представляет собой расширяемую кодовую базу, которая определяет точки подключения для вставки нашего собственного кода.

В свою очередь, фреймворк вызывает наш код через один или несколько специализированных подклассов, используя реализации интерфейсов и аннотации. Фреймворк Spring — хороший пример последнего подхода.

3. Основы DIP

Чтобы понять мотивы DIP, давайте начнем с его формального определения, данного Робертом С. Мартином в его книге Agile Software Development: Principles, Patterns, and Practices:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend on details. Details should depend on abstractions.

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

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

3.1. Варианты дизайна и DIP

Рассмотрим простой класс StringProcessor, который получает значение String с помощью компонента StringReader и записывает его в другое место с помощью компонента StringWriter:

public class StringProcessor {
    
    private final StringReader stringReader;
    private final StringWriter stringWriter;
    
    public StringProcessor(StringReader stringReader, StringWriter stringWriter) {
        this.stringReader = stringReader;
        this.stringWriter = stringWriter;
    }

    public void printString() {
        stringWriter.write(stringReader.getValue());
    }
}

Хотя реализация класса StringProcessor является базовой, Есть несколько вариантов дизайна, которые мы можем сделать здесь.

Давайте разобьем каждый вариант дизайна на отдельные элементы, чтобы четко понять, как каждый из них может повлиять на общий дизайн:

  1. StringReader and StringWriter, the low-level components, are concrete classes placed in the same package. StringProcessor, the high-level component is placed in a different package. StringProcessor depends on StringReader and StringWriter. There is no inversion of dependencies, hence StringProcessor is not reusable in a different context.
  2. StringReader and StringWriter are interfaces placed in the same package along with the implementations. StringProcessor now depends on abstractions, but the low-level components don’t. We have not achieved inversion of dependencies yet.
  3. StringReader and StringWriter are interfaces placed in the same package together with StringProcessor. Now, StringProcessor has the explicit ownership of the abstractions. StringProcessor, StringReader, and StringWriter all depend on abstractions. We have achieved inversion of dependencies from top to bottom by abstracting the interaction between the components. StringProcessor is now reusable in a different context.
  4. StringReader and StringWriter are interfaces placed in a separate package from StringProcessor. We achieved inversion of dependencies, and it’s also easier to replace StringReader and StringWriter implementations. StringProcessor is also reusable in a different context.

Из всех приведенных выше сценариев только пункты 3 и 4 являются допустимыми реализациями DIP.

3.2. Определение владения абстракциями

Пункт 3 представляет собой прямую реализацию DIP, где высокоуровневый компонент и абстракция(и) помещаются в один и тот же пакет. Следовательно, компонент высокого уровня владеет абстракциями. В этой реализации высокоуровневый компонент отвечает за определение абстрактного протокола, посредством которого он взаимодействует с низкоуровневыми компонентами.

Аналогично, пункт 4 представляет собой более развязанную реализацию DIP. В этом варианте паттерна ни высокоуровневые, ни низкоуровневые компоненты не владеют абстракциями.

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

3.3. Выбор правильного уровня абстракции

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

В приведенном выше примере мы использовали DI для внедрения типа StringReader в класс StringProcessor. Это будет эффективно до тех пор, пока уровень абстракции StringReader близок к домену StringProcessor.

Напротив, мы бы просто упустили внутренние преимущества DIP, если бы StringReader был, например, объектом File, который считывает значение String из файла. В этом случае уровень абстракции StringReader будет намного ниже, чем уровень домена StringProcessor.

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

4. Реализации Java 8

Мы уже подробно рассмотрели ключевые концепции DIP, поэтому теперь рассмотрим несколько практических реализаций шаблона в Java 8.

4.1. Прямая реализация DIP

Давайте создадим демонстрационное приложение, которое извлекает некоторых клиентов из уровня постоянства и обрабатывает их каким-то дополнительным способом.

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

Давайте начнем с определения высокоуровневого компонента:

public class CustomerService {

    private final CustomerDao customerDao;

    // standard constructor / getter

    public Optional<Customer> findById(int id) {
        return customerDao.findById(id);
    }

    public List<Customer> findAll() {
        return customerDao.findAll();
    }
}

Как мы видим, класс CustomerService реализует методы findById() и findAll(), которые извлекают клиентов из уровня персистентности, используя простую реализацию DAO. . Конечно, мы могли бы инкапсулировать в классе больше функций, но для простоты давайте оставим все как есть.

В данном случае тип CustomerDao — это абстракция, которую CustomerService использует для использования низкоуровневого компонента.

Поскольку это прямая реализация DIP, давайте определим абстракцию как интерфейс в том же пакете CustomerService:

public interface CustomerDao {

    Optional<Customer> findById(int id);

    List<Customer> findAll();

}

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

Кроме того, уровень абстракции CustomerDao близок к уровню CustomerService, что также необходимо для хорошей реализации DIP.

Теперь давайте создадим низкоуровневый компонент в другом пакете. В данном случае это просто базовая реализация CustomerDao:

public class SimpleCustomerDao implements CustomerDao {

    // standard constructor / getter

    @Override
    public Optional<Customer> findById(int id) {
        return Optional.ofNullable(customers.get(id));
    }

    @Override
    public List<Customer> findAll() {
        return new ArrayList<>(customers.values());
    }
}

Наконец, давайте создадим модульный тест для проверки функциональности класса CustomerService:

@Before
public void setUpCustomerServiceInstance() {
    var customers = new HashMap<Integer, Customer>();
    customers.put(1, new Customer("John"));
    customers.put(2, new Customer("Susan"));
    customerService = new CustomerService(new SimpleCustomerDao(customers));
}

@Test
public void givenCustomerServiceInstance_whenCalledFindById_thenCorrect() {
    assertThat(customerService.findById(1)).isInstanceOf(Optional.class);
}

@Test
public void givenCustomerServiceInstance_whenCalledFindAll_thenCorrect() {
    assertThat(customerService.findAll()).isInstanceOf(List.class);
}

@Test
public void givenCustomerServiceInstance_whenCalledFindByIdWithNullCustomer_thenCorrect() {
    var customers = new HashMap<Integer, Customer>();
    customers.put(1, null);
    customerService = new CustomerService(new SimpleCustomerDao(customers));
    Customer customer = customerService.findById(1).orElseGet(() -> new Customer("Non-existing customer"));
    assertThat(customer.getName()).isEqualTo("Non-existing customer");
}

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

Кроме того, на следующей диаграмме показана структура нашего демонстрационного приложения, от высокоуровневого до низкоуровневого пакета:

4.2. Альтернативная реализация DIP

Как мы обсуждали ранее, можно использовать альтернативную реализацию DIP, в которой мы размещаем высокоуровневые компоненты, абстракции и низкоуровневые компоненты в разных пакетах.

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

Конечно, реализация этого варианта шаблона сводится к размещению CustomerService, MapCustomerDao и CustomerDao в отдельных пакетах.

Таким образом, схемы достаточно, чтобы показать, как устроен каждый компонент в этой реализации:

5. Модульная реализация Java 11

Преобразовать наше демонстрационное приложение в модульное довольно просто.

Это действительно хороший способ продемонстрировать, как JPMS обеспечивает соблюдение лучших практик программирования, включая строгую инкапсуляцию, абстракцию и повторное использование компонентов через DIP.

«Нам не нужно заново реализовывать наши образцы компонентов с нуля. Следовательно, модульность нашего примера приложения — это всего лишь вопрос размещения каждого файла компонента в отдельном модуле вместе с соответствующим дескриптором модуля.

Вот как будет выглядеть модульная структура проекта:

project base directory (could be anything, like dipmodular)
|- com.baeldung.dip.services
   module-info.java
     |- com
       |- baeldung
         |- dip
           |- services
             CustomerService.java
|- com.baeldung.dip.daos
   module-info.java
     |- com
       |- baeldung
         |- dip
           |- daos
             CustomerDao.java
|- com.baeldung.dip.daoimplementations 
    module-info.java 
      |- com 
        |- baeldung 
          |- dip 
            |- daoimplementations 
              SimpleCustomerDao.java  
|- com.baeldung.dip.entities
    module-info.java
      |- com
        |- baeldung
          |- dip
            |- entities
              Customer.java
|- com.baeldung.dip.mainapp 
    module-info.java 
      |- com 
        |- baeldung 
          |- dip 
            |- mainapp
              MainApplication.java

5.1. Модуль компонента высокого уровня

Давайте начнем с размещения класса CustomerService в отдельном модуле.

Мы создадим этот модуль в корневом каталоге com.baeldung.dip.services и добавим дескриптор модуля, module-info.java:

module com.baeldung.dip.services {
    requires com.baeldung.dip.entities;
    requires com.baeldung.dip.daos;
    uses com.baeldung.dip.daos.CustomerDao;
    exports com.baeldung.dip.services;
}

По понятным причинам мы не будем углубляться в подробности о том, как работает JPMS. Тем не менее, ясно увидеть зависимости модуля, просто взглянув на директивы require.

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

Конечно, нам еще нужно разместить высокоуровневый компонент, класс CustomerService, в этом модуле. Итак, в корневом каталоге com.baeldung.dip.services создадим следующую структуру каталогов, похожую на пакет: com/baeldung/dip/services.

Наконец, давайте поместим файл CustomerService.java в этот каталог.

5.2. Модуль абстракции

Точно так же нам нужно поместить интерфейс CustomerDao в отдельный модуль. Поэтому давайте создадим модуль в корневом каталоге com.baeldung.dip.daos и добавим дескриптор модуля:

module com.baeldung.dip.daos {
    requires com.baeldung.dip.entities;
    exports com.baeldung.dip.daos;
}

Теперь давайте перейдем в каталог com.baeldung.dip.daos и создадим следующую структуру каталогов. : com/baeldung/dip/daos. Давайте поместим файл CustomerDao.java в этот каталог.

5.3. Низкоуровневый компонентный модуль

По логике вещей, низкоуровневый компонент SimpleCustomerDao также нужно поместить в отдельный модуль. Как и ожидалось, процесс очень похож на то, что мы только что сделали с другими модулями.

Давайте создадим новый модуль в корневом каталоге com.baeldung.dip.daoimplementations и включим дескриптор модуля:

module com.baeldung.dip.daoimplementations {
    requires com.baeldung.dip.entities;
    requires com.baeldung.dip.daos;
    provides com.baeldung.dip.daos.CustomerDao with com.baeldung.dip.daoimplementations.SimpleCustomerDao;
    exports com.baeldung.dip.daoimplementations;
}

В контексте JPMS это модуль поставщика услуг, поскольку он объявляет функции предоставления и с директивами.

В этом случае модуль делает службу CustomerDao доступной для одного или нескольких потребительских модулей через реализацию SimpleCustomerDao.

Не будем забывать, что наш потребительский модуль com.baeldung.dip.services использует эту службу через директиву uses.

Это ясно показывает, насколько просто иметь прямую реализацию DIP с помощью JPMS, просто определяя потребителей, поставщиков услуг и абстракции в разных модулях.

Точно так же нам нужно поместить файл SimpleCustomerDao.java в этот новый модуль. Давайте перейдем в каталог com.baeldung.dip.daoimplementations и создадим новую структуру каталогов, подобную пакету, с этим именем: com/baeldung/dip/daoimplementations.

Наконец, давайте поместим файл SimpleCustomerDao.java в каталог.

5.4. Модуль Entity

Кроме того, нам нужно создать еще один модуль, в который мы можем поместить класс Customer.java. Как и раньше, давайте создадим корневой каталог com.baeldung.dip.entities и включим дескриптор модуля:

module com.baeldung.dip.entities {
    exports com.baeldung.dip.entities;
}

В корневом каталоге пакета создадим каталог com/baeldung/dip/entities и добавим следующее Файл Customer.java:

public class Customer {

    private final String name;

    // standard constructor / getter / toString
    
}

5.5. Основной модуль приложения

Далее нам нужно создать дополнительный модуль, который позволит нам определить точку входа нашего демонстрационного приложения. Поэтому давайте создадим еще один корневой каталог com.baeldung.dip.mainapp и поместим в него дескриптор модуля:

module com.baeldung.dip.mainapp {
    requires com.baeldung.dip.entities;
    requires com.baeldung.dip.daos;
    requires com.baeldung.dip.daoimplementations;
    requires com.baeldung.dip.services;
    exports com.baeldung.dip.mainapp;
}

Теперь давайте перейдем к корневому каталогу модуля и создадим следующую структуру каталогов: com/baeldung/dip /основное приложение. В этот каталог добавим файл MainApplication.java, который просто реализует метод main():

public class MainApplication {

    public static void main(String args[]) {
        var customers = new HashMap<Integer, Customer>();
        customers.put(1, new Customer("John"));
        customers.put(2, new Customer("Susan"));
        CustomerService customerService = new CustomerService(new SimpleCustomerDao(customers));
        customerService.findAll().forEach(System.out::println);
    }
}

Наконец, давайте скомпилируем и запустим демонстрационное приложение — либо из нашей IDE, либо из командной консоли.

Как и ожидалось, мы должны увидеть список объектов Customer, выводимых на консоль при запуске приложения:

Customer{name=John}
Customer{name=Susan}

«

«Кроме того, на следующей диаграмме показаны зависимости каждого модуля приложения:

6. Заключение

В этом руководстве мы подробно рассмотрели ключевые концепции DIP, а также показали различные реализации шаблона в Java 8 и Java 11, причем последняя использует JPMS.