Статьи

Контейнеры DI являются загрязнителями кода

В то время как внедрение зависимостей (также называемое «DI») является естественной техникой создания объектов в ООП (известной задолго до того, как термин был введен Мартином Фаулером ), Spring IoC , Google Guice , Java EE6 CDI , Dagger и другие структуры DI превращают его в анти-шаблон.

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

Вместо этого давайте поговорим о «внедрении конструктора» (как в Google Guice ) и его использовании с контейнерами внедрения зависимостей . Я попытаюсь показать, почему я считаю эти контейнеры избыточными, по крайней мере.

Что такое инъекция зависимостей?

Вот что такое внедрение зависимостей (на самом деле не отличается от простой старой композиции объектов):

01
02
03
04
05
06
07
08
09
10
11
public class Budget {
  private final DB db;
  public Budget(DB data) {
    this.db = data;
  }
  public long total() {
    return this.db.cell(
      "SELECT SUM(cost) FROM ledger"
    );
  }
}

Данные объекта называются «зависимостью».

Budget не знает, с какой базой данных он работает. Все, что ему нужно из базы данных, — это ее способность извлекать ячейку, используя произвольный запрос SQL, через метод cell() . Мы можем создать экземпляр Budget с помощью реализации интерфейса DB в PostgreSQL, например:

1
2
3
4
5
6
7
8
public class App {
  public static void main(String... args) {
    Budget budget = new Budget(
      new Postgres("jdbc:postgresql:5740/main")
    );
    System.out.println("Total is: " + budget.total());
  }
}

Другими словами, мы «внедряем» зависимость в новый budget объекта.

Альтернативой этому подходу «внедрения зависимостей» было бы позволить Budget решить, с какой базой данных он хочет работать:

1
2
3
4
public class Budget {
  private final DB db = new Postgres("jdbc:postgresql:5740/main");
  // class methods
}

Это очень грязно и приводит к 1) дублированию кода, 2) невозможности повторного использования и 3) невозможности тестирования и т. Д. Не нужно обсуждать почему. Это очевидно.

Таким образом, внедрение зависимостей через конструктор является удивительной техникой. Ну, даже не техника, правда. Больше похоже на особенность Java и всех других объектно-ориентированных языков. Ожидается, что почти любой объект захочет инкапсулировать некоторые знания (иначе говоря, «состояние»). Для этого и нужны конструкторы.

Что такое DI-контейнер?

Пока все хорошо, но здесь идет темная сторона — контейнер для инъекций зависимости. Вот как это работает (в качестве примера рассмотрим Google Guice):

1
2
3
4
5
6
7
8
9
import javax.inject.Inject;
public class Budget {
  private final DB db;
  @Inject
  public Budget(DB data) {
    this.db = data;
  }
  // same methods as above
}

Обратите внимание: конструктор аннотирован @Inject .

Затем мы должны сконфигурировать контейнер где-нибудь, когда приложение запустится:

01
02
03
04
05
06
07
08
09
10
Injector injector = Guice.createInjector(
  new AbstractModule() {
    @Override
    public void configure() {
      this.bind(DB.class).toInstance(
        new Postgres("jdbc:postgresql:5740/main")
      );
    }
  }
);

Некоторые фреймворки даже позволяют нам конфигурировать инжектор в XML-файле.

Отныне нам не разрешается создавать экземпляры Budget через new оператора, как мы это делали раньше. Вместо этого мы должны использовать только что созданный инжектор:

1
2
3
4
5
6
7
public class App {
  public static void main(String... args) {
    Injection injector = // as we just did in the previous snippet
    Budget budget = injector.getInstance(Budget.class);
    System.out.println("Total is: " + budget.total());
  }
}

Инъекция автоматически обнаруживает, что для создания экземпляра Budget необходимо указать аргумент для своего конструктора. Он будет использовать экземпляр класса Postgres , который мы создали в инжекторе.

Это правильный и рекомендуемый способ использования Guice. Тем не менее, есть еще несколько более темных рисунков, которые возможны, но не рекомендуются. Например, вы можете сделать свой инжектор синглтоном и использовать его прямо внутри класса Budget . Однако эти механизмы считаются неправильными даже производителями DI-контейнеров, поэтому давайте проигнорируем их и сосредоточимся на рекомендуемом сценарии.

Для чего это?

Позвольте мне повторить и обобщить сценарии некорректного использования контейнеров внедрения зависимостей:

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

Если мы отложим все их в сторону, то останется только инъекция конструктора, описанная выше. И как это поможет нам? Зачем нам это нужно? Почему мы не можем использовать старый добрый new в главном классе приложения?

Контейнер, который мы создали, просто добавляет больше строк в базу кода или даже больше файлов, если мы используем XML. И это ничего не добавляет, кроме дополнительной сложности. Мы всегда должны помнить об этом, если у нас возникает вопрос: «Какая база данных используется в качестве аргумента бюджета?»

Правильный путь

Теперь позвольте мне показать вам реальный пример использования new для создания приложения. Вот как мы создаем «механизм мышления» в rultor.com (полный класс в Agents.java ):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
final Agent agent = new Agent.Iterative(
  new Array(
    new Understands(
      this.github,
      new QnSince(
        49092213,
        new QnReferredTo(
          this.github.users().self().login(),
          new QnParametrized(
            new Question.FirstOf(
              new Array(
                new QnIfContains("config", new QnConfig(profile)),
                new QnIfContains("status", new QnStatus(talk)),
                new QnIfContains("version", new QnVersion()),
                new QnIfContains("hello", new QnHello()),
                new QnIfCollaborator(
                  new QnAlone(
                    talk, locks,
                    new Question.FirstOf(
                      new Array(
                        new QnIfContains(
                          "merge",
                          new QnAskedBy(
                            profile,
                            Agents.commanders("merge"),
                            new QnMerge()
                          )
                        ),
                        new QnIfContains(
                          "deploy",
                          new QnAskedBy(
                            profile,
                            Agents.commanders("deploy"),
                            new QnDeploy()
                          )
                        ),
                        new QnIfContains(
                          "release",
                          new QnAskedBy(
                            profile,
                            Agents.commanders("release"),
                            new QnRelease()
                          )
                        )
                      )
                    )
                  )
                )
              )
            )
          )
        )
      )
    ),
    new StartsRequest(profile),
    new RegistersShell(
      "b1.rultor.com", 22,
      "rultor",
      IOUtils.toString(
        this.getClass().getResourceAsStream("rultor.key"),
        CharEncoding.UTF_8
      )
    ),
    new StartsDaemon(profile),
    new KillsDaemon(TimeUnit.HOURS.toMinutes(2L)),
    new EndsDaemon(),
    new EndsRequest(),
    new Tweets(
      this.github,
      new OAuthTwitter(
        Manifests.read("Rultor-TwitterKey"),
        Manifests.read("Rultor-TwitterSecret"),
        Manifests.read("Rultor-TwitterToken"),
        Manifests.read("Rultor-TwitterTokenSecret")
      )
    ),
    new CommentsTag(this.github),
    new Reports(this.github),
    new RemovesShell(),
    new ArchivesDaemon(
      new ReRegion(
        new Region.Simple(
          Manifests.read("Rultor-S3Key"),
          Manifests.read("Rultor-S3Secret")
        )
      ).bucket(Manifests.read("Rultor-S3Bucket"))
    ),
    new Publishes(profile)
  )
);

Впечатляет? Это настоящая объектная композиция. Я полагаю, что именно так должно быть создано надлежащее объектно-ориентированное приложение.

А DI контейнеры? На мой взгляд, они просто добавляют ненужный шум.

Похожие сообщения

Вы также можете найти эти сообщения интересными:

Ссылка: Контейнеры DI являются загрязнителями кода от нашего партнера по JCG Егора Бугаенко в блоге About Programming .