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


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

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





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


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

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


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

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 {

  protected DataSource dataSource = null;

  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;
    
    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;

    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.

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

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