В то время как внедрение зависимостей (также называемое «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 контейнеры? На мой взгляд, они просто добавляют ненужный шум.
Похожие сообщения
Вы также можете найти эти сообщения интересными:
- Геттеры / сеттеры. Злой. Период.
- Анти-паттерны в ООП
- Избегайте конкатенации строк
- Объекты должны быть неизменными
- Почему NULL это плохо?
Ссылка: | Контейнеры DI являются загрязнителями кода от нашего партнера по JCG Егора Бугаенко в блоге About Programming . |