От переводчика


Представляемый вашему вниманию перевод открывает серию статей от Jakob Jenkov, посвященных внедрению зависимостей, или DI. Примечательна серия тем, что в ней автор, анализируя понятия и практическое применение таких понятий как «зависимость», «внедрение зависимостей», «контейнер для внедрения зависимостей», сравнивая паттерны создания объектов, анализируя недостатки конкретных реализаций DI-контейнеров (например, Spring), рассказывает, как пришел к написанию собственного DI-контейнера. Таким образом, читателю предлагается познакомиться с довольно цельным взглядом на вопрос управления зависимостями в приложениях.

В данной статье сравнивается подход к настройке объектов изнутри и извне (DI). По смыслу настоящая статья продолжает статью Jakob Jenkov Understanding Dependencies, в которой дается определение самому понятию «зависимости» и их типам.



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


«Внедрение зависимостей» — это выражение, впервые использованное в статье Мартина Фаулера Inversion of Control Containers and the Dependency Injection Pattern. Это хорошая статья, но она упускает некоторые преимущества контейнеров внедрения зависимостей. Также я не согласен с выводами статьи, но больше об этом — в следующих текстах.

Объяснение внедрения зависимостей


Видео от автора с наглядными примерами по тексту статьи
Внедрение зависимостей — это стиль настройки объекта, при котором поля объекта задаются внешней сущностью. Другими словами, объекты настраиваются внешними объектами. DI — это альтернатива самонастройке объектов. Это может выглядеть несколько абстрактно, так что посмотрим пример:

public class MyDao {

    protected DataSource dataSource =
    new DataSourceImpl("driver", "url", "user", "password");

    //data access methods...
    public Person readPerson(int primaryKey) {...}

  }

Этот DAO (Data Access Object), MyDao нуждается в экземпляре javax.sql.DataSource для того, чтобы получить подключения к базе данных. Подключения к БД используются для чтения и записи в БД, например, объектов Person.

Заметьте, что класс MyDao создает экземпляр DataSourceImpl, так как нуждается в источнике данных. Тот факт, что MyDao нуждается в реализации DataSource, означает, что он зависит от него. Он не может выполнить свою работу без реализации DataSource. Следовательно, MyDao имеет «зависимость» от интерфейса DataSource и от какой-то его реализации.

Класс MyDao создает экземпляр DataSourceImpl как реализацию DataSource. Следовательно, класс MyDao сам «разрешает свои зависимости». Когда класс разрешает собственные зависимости, он автоматически также зависит от классов, для которых он разрешает зависимости. В данном случае MyDao завсист также от DataSourceImpl и от четырех жестко заданных строковых значений, передаваемых в конструктор DataSourceImpl. Вы не можете ни использовать другие значения для этих четырех строк, ни использовать другую реализацию интерфейса DataSource без изменения кода.

Как вы можете видеть, когда класс разрешает собственные зависимости, он становится негибким в отношении к этим зависимостям. Это плохо. Это значит, что если вам нужно поменять зависимости, вам нужно поменять код. В данном примере это означает, что если вам нужно использовать другую базу данных, вам потребуется поменять класс MyDao. Если у вас много DAO-классов, реализованных таким образом, вам придется изменять их все. В добавок, вы не можете провести юнит-тестирование MyDao, замокав реализацию DataSource. Вы можете использовать только DataSourceImpl. Не требуется много ума, чтобы понять, что это плохая идея.

Давайте немного поменяем дизайн:

public class MyDao {

  //UPD: protected DataSource dataSource = null; - не надо так
  protected DataSource dataSource;

  public MyDao(String driver, String url, String user, String password){
    this.dataSource = new DataSourceImpl(driver, url, user, password);
  }

  //data access methods...
  public Person readPerson(int primaryKey) {...}

}

Заметьте, что создание экземпляра DataSourceImpl перемещено в конструктор. Конструктор принимает четыре параметра, это — четыре значения, необходимые для DataSourceImpl. Хотя класс MyDao все еще зависит от этих четырех значений, он больше не разрешает зависимости сам. Они предоставляются классом, создающим экземпляр MyDao. Зависимости «внедряются» в конструктор MyDao. Отсюда и термин «внедрение (прим. перев.: или иначе — инъекция) зависимостей». Теперь возможно сменить драйвер БД, URL, имя пользователя или пароль, используемый классом MyDao без его изменения.

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

Класс MyDao все еще может быть более независимым. Он все еще зависит и от интерфейса DataSource, и от класса DataSourceImpl. Нет необходимости зависеть от чего-то, кроме интерфейса DataSource. Это может быть достигнуто инъекцией DataSource в конструктор вместо четырех параметров строкового типа. Вот как это выглядит:

public class MyDao {

    //UPD:  protected DataSource dataSource = null; - не надо так
    protected DataSource dataSource;
    
    public MyDao(DataSource dataSource){
      this.dataSource = dataSource;
    }

    //data access methods...
    public Person readPerson(int primaryKey) {...}

  }

Теперь класс MyDao больше не зависит от класса DataSourceImpl или от четырех строк, необходимых конструктору DataSourceImpl. Теперь можно использовать любую реализацию DataSource в конструкторе MyDao.

Цепное внедрение зависимостей


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

public class MyBizComponent{
    public void changePersonStatus(Person person, String status){

       MyDao dao = new MyDao(
            new DataSourceImpl("driver", "url", "user", "password"));

       Person person = dao.readPerson(person.getId());
       person.setStatus(status);
       dao.update(person);
    }
  }

Как вы можете видеть, теперь MyBizComponent зависит от класса DataSourceImpl и четырех строк, необходимых его конструктору. Это еще хуже, чем зависимость MyDao от них, потому что MyBizComponent теперь зависит от классов и информации, которую он даже сам не использует. Более того, реализация DataSourceImpl и параметры конструктора принадлежат к разным слоям абстракции. Слой ниже MyBizComponent — это слой DAO.

Решение — продолжить внедрение зависимости по всем слоям. MyBizComponent должен зависеть только от экземпляра MyDao. Вот как это выглядит:

 public class MyBizComponent{

    //UPD: protected MyDao dao = null;
    protected MyDao dao;

    public MyBizComponent(MyDao dao){
       this.dao = dao;
    }
    
    public void changePersonStatus(Person person, String status){
       Person person = dao.readPerson(person.getId());
       person.setStatus(status);
       dao.update(person);
    }
  }

Снова зависимость, MyDao, предоставляется через конструктор. Теперь MyBizComponent зависит только от класса MyDao. Если бы MyDao был интерфейсом, можно было бы менять реализацию без ведома MyBizComponent.

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

Комментарии (9)


  1. flatscode
    27.02.2018 13:04

    protected DataSource dataSource = null;
    protected MyDao dao = null;

    А вот как назвать человека, который так пишет?


  1. Kiselioff Автор
    27.02.2018 13:19

    Как Вам угодно. Как бы Вы назвали? И как это связано с темой статьи?


    1. flatscode
      27.02.2018 17:09
      +1

      Как бы Вы назвали?

      Назвал бы чайником, который только неделю назад начал изучать Java.

      И даже для этих классов генерируется разный байт-код:

      public class A {
          public Object value = null;
      }


      public class B {
          public Object value;
      }


      Байткод для A:
      // class version 52.0 (52)
      // access flags 0x21
      public class A {
      
        // compiled from: A.java
      
        // access flags 0x1
        public Ljava/lang/Object; value
      
        // access flags 0x1
        public <init>()V
         L0
          LINENUMBER 3 L0
          ALOAD 0
          INVOKESPECIAL java/lang/Object.<init> ()V
         L1
          LINENUMBER 5 L1
          ALOAD 0
          ACONST_NULL
          PUTFIELD A.value : Ljava/lang/Object;
          RETURN
         L2
          LOCALVARIABLE this LA; L0 L2 0
          MAXSTACK = 2
          MAXLOCALS = 1
      }


      Байткод для B:
      // class version 52.0 (52)
      // access flags 0x21
      public class B {
      
        // compiled from: B.java
      
        // access flags 0x1
        public Ljava/lang/Object; value
      
        // access flags 0x1
        public <init>()V
         L0
          LINENUMBER 3 L0
          ALOAD 0
          INVOKESPECIAL java/lang/Object.<init> ()V
          RETURN
         L1
          LOCALVARIABLE this LB; L0 L1 0
          MAXSTACK = 1
          MAXLOCALS = 1
      }


      1. Kiselioff Автор
        27.02.2018 17:49

        Ваша правда. Не надо так. Код дан без редактирования, полностью сохранен авторский. Даже не заметил. Спасибо, что указали.


        1. Kiselioff Автор
          27.02.2018 18:51

          Поправил код.


          1. fogone
            28.02.2018 06:43
            +1

            Разве эти поля не должны быть private final? В этом одно из преимуществ внедрения в конструктор.


            1. Kiselioff Автор
              28.02.2018 08:48

              Если писать идеально, то да. Изначально для меня идея перевода включала в себя принцип не делать текст ни лучше, ни хуже, чем он есть на самом деле. Это касается не только основного текста, но и кода тоже. Основную идею статьи код, на мой взгляд, иллюстрирует. Действительно, он не идеален. Однако, автор написал так как он написал. На данный момент я вижу такой выход как сделать апдейт с явным комментарием от переводчика, который явно укажет на недостатки авторского кода. П.С. основное преимущество текстов от Jakob Jenkov — это простота и доходчивость. Давайте вместе отмечать недостатки и делать текст лучше.


              1. wisznewecki
                28.02.2018 11:42

                да все нормально, выкидывайте из черновиков


                1. Kiselioff Автор
                  28.02.2018 14:02

                  Спасибо. Пробовал это сделать, кнопка «удалить» не активна. Обратился в поддержку. Оказывается, нельзя удалить черновик статьи, которая уже была опубликована и затем скрыта.