Dependency injection

Автор оригинала: Dependency injection
  • Перевод

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


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

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




Серия включает в себя следующие статьи



  1. Dependency Injection
  2. Dependency Injection Containers
  3. Dependency Injection Benefits
  4. When to use Dependency Injection
  5. Is Dependency Injection Replacing the Factory Patterns?


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


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

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


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

UPD: после обсуждения представленных автором фрагментов кода с flatscode и fogone, я принял решение скорректировать спорные моменты в коде. Изначальный замысел был в том, чтобы не трогать код и давать его таким, каков он написан автором. Оригинальный авторский код в спорных местах закомментирован с указанием «в оригинале», ниже дается его исправленная версия. Также оригинальный код можно найти по ссылке в начале статьи.

public class MyDao {
 //в оригинале: protected DataSource dataSource =
    private 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 {

  //в оригинале: protected DataSource dataSource = null;
  private final 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 {

    //в оригинале:  protected DataSource dataSource = null;
    private final 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{

    //в оригинале: protected MyDao dao = null;
    private final 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.

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

К началу
Поделиться публикацией

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 12

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

    А вот как назвать человека, который так пишет?
      +2
      Как Вам угодно. Как бы Вы назвали? И как это связано с темой статьи?
        +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
        }
          0
          Ваша правда. Не надо так. Код дан без редактирования, полностью сохранен авторский. Даже не заметил. Спасибо, что указали.
            0
            Поправил код.
              +2
              Разве эти поля не должны быть private final? В этом одно из преимуществ внедрения в конструктор.
                0
                Если писать идеально, то да. Изначально для меня идея перевода включала в себя принцип не делать текст ни лучше, ни хуже, чем он есть на самом деле. Это касается не только основного текста, но и кода тоже. Основную идею статьи код, на мой взгляд, иллюстрирует. Действительно, он не идеален. Однако, автор написал так как он написал. На данный момент я вижу такой выход как сделать апдейт с явным комментарием от переводчика, который явно укажет на недостатки авторского кода. П.С. основное преимущество текстов от Jakob Jenkov — это простота и доходчивость. Давайте вместе отмечать недостатки и делать текст лучше.
                  +1
                  да все нормально, выкидывайте из черновиков
                    0
                    Спасибо. Пробовал это сделать, кнопка «удалить» не активна. Обратился в поддержку. Оказывается, нельзя удалить черновик статьи, которая уже была опубликована и затем скрыта.
                      0
                      ждем продолжений
                        0
                        Рад. Планирую завтра выдать еще две «главы»
                          0
                          Сегодня, как и обещал, опубликовал следующую часть о DI-контейнерах и преимуществах их использования.

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

        Самое читаемое