-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathDependencyInjectionContainers.txt
165 lines (126 loc) · 17.1 KB
/
DependencyInjectionContainers.txt
1
2
3
4
5
6
7
8
9
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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
Источник: https://habr.com/ru/articles/350708/
****** Контейнеры внедрения зависимостей ******
Если в вашей системе все компоненты имеют свои зависимости, то где-то в системе какой-то класс
или фабрика должны знать, что внедрять во все эти компоненты. Вот что делает DI-контейнер.
Причина, по которой это называется «контейнер», а не «фабрика» в том, что контейнер обычно
берет на себя ответственность не только за создание экземпляров, но и внедрение зависимостей.
Когда вы конфигурируете DI-контейнер, вы определяете, экземпляры каких компонентов он должен
быть способен создать, и какие зависимости внедрить в каждый компонент. Также вы обычно можете
настроить режим создания экземпляра для каждого компонента. Например, должен ли новый экземпляр
создаваться каждый раз? Или один и тот же экземпляр компонента должен быть переиспользован
(синглтон) везде, куда он внедряется?
Если некоторые компоненты настроены как синглтоны, то некоторые контейнеры имеют возможность
вызывать методы синглтона тогда, когда контейнер выключается. Таким образом синглтон может
освободить любые ресурсы, которые он использует, такие, как подключение к БД или сетевое
соединение.
Такой процесс называют «управлением жизненным циклом объекта». Это значит, что контейнер способен
управлять компонентом на различных стадиях жизненного цикла компонента.
Например, создание, конфигурирование и удаление.
Управление жизненным циклом — это одна из обязанностей, которую DI контейнеры принимают в дополнение
к созданию экземпляров и их внедрению. Тот факт, что контейнер иногда сохраняет ссылку на компоненты
после создания экземпляра, и есть та причина, по которой он называется «контейнером», а не фабрикой.
DI-контейнеры обычно сохраняют ссылки на объекты, чьим жизненным циклом им предстоит управлять, или,
которые будут переиспользованы для будущих внедрений, такие, как синглтон или приспособленец.
Когда контейнер настроен на создание новых экземпляров компонентов при каждом вызове, контейнер обычно
«забывает» о созданных объектах. В противном случае у сборщика мусора будет горячая пора, когда придет
время собирать все эти объекты.
На данный момент доступно несколько DI-контейнеров. Для Java существуют Butterfly Container, Spring,
Pico Container (в его разработке участвовал Мартин Фаулер), Guice (разработка Google) и другие
(например, есть еще Dagger, также разработка Google. Jakob Jenkov, автор данной переведенной статьи,
разработал Butterfly Container. Его исходный код доступен на github, а документация содержится в
отдельной серии постов).
*** Выгоды от использования DI и DI-контейнеров ***
Существует несколько преимуществ от использования DI-контейнеров по сравнению с тем, что компонентам
приходится самостоятельно разрешать свои зависимости («самостоятельно разрешать зависимости» в данном
контексте означает «создавать объекты, необходимые для работы компонента, внутри самого компонента»).
Некоторые из этих преимуществ:
- Меньше зависимостей;
- Меньше «перенос» зависимостей;
- Код проще переиспользовать;
- Код удобнее тестировать;
- Код удобнее читать;
*** Меньше зависимостей ***
DI делает возможным устранить или, по крайней мере, уменьшить необязательные зависимости компонента.
Компонент уязвим перед изменением его зависимостей. Если зависимость изменится, компоненту, возможно,
придется адаптироваться к этим изменениям.
Например, если сигнатура метода зависимости изменится, компоненту придется изменять вызов этого метода.
Когда зависимости компонента сведены к минимуму, он в меньшей степени подвержен необходимости изменений.
*** Код проще переиспользовать ***
Снижение числа зависимостей компонента обычно делает проще его переиспользование в другом контексте.
Тот факт, что зависимости могут быть внедрены и, следовательно, сконфигурированы внешне, повышает
возможность переиспользования этого компонента. Если в другом контексте требуется другая реализация
какого-либо интерфейса, или другая конфигурация той же самой реализации, компонент может быть
сконфигурирован для работы с этой реализацией. При этом нет необходимости изменять код.
*** Код удобнее тестировать ***
DI также повышает возможности тестирования компонентов. Когда зависимости могут быть внедрены в
компонент, возможно также и внедрение mock-ов этих объектов. Mock-объекты используются для тестирования
как замена настоящей имплементации. Поведение mock-объекта может быть сконфигурировано. Таким образом,
все возможное поведение компонента при использовании mock-объекта может быть протестировано на
корректность.
Например, обработка ситуации, когда mock возвращает корректный объект, когда возвращает null и когда
выбрасывается исключение. Кроме того, mock-объекты обычно записывают, какие их методы были вызваны.
Таким образом, тест может проверить, что компонент, использующий mock, использовал вызванные методы
как ожидалось.
*** Код удобнее читать ***
DI переносит зависимости в интерфейс компонентов. Это делает нагляднее то, какие зависимости есть
у компонента, делая код более удобным для чтения. Вам не придется просматривать весь код для того,
чтобы увидеть то, какие зависимости вам нужно будет предоставить для данного компонента. Они все
видны в интерфейсе.
*** Меньше «перенос» зависимостей ***
Еще один приятный бонус, DI избавляет от того, что называется «перенос зависимостей». Перенос
зависимостей проявляется в том, что объект получает параметр в одном из своих методов, который
сам по себе объекту не нужен, а нужен одному из объектов, которые он вызывает для своей работы.
Это может звучать немного абстрактно, так что давайте приведем простой пример.
Компонент 'A' загружает приложение и создает объект конфигурации - 'Config', который нужен какому-то
из объектов приложения, но не всем компонентам в системе. Затем 'А' вызывает 'B', 'B' вызывает 'C',
'С' вызывает 'D'. НО ни 'B', ни 'C' не нуждаются в объекте типа 'Config', а вот 'D' нуждается. Вот
цепочка вызовов.
****************************************************************************************************
A создает Config
A --> B --> C --> D --> Config
****************************************************************************************************
Стрелки символизируют вызовы методов. Если 'A' создает 'B', 'B' создает 'C', 'C' создает 'D', и только
'D' нуждается в 'Config', то объект 'Config' должен быть передан через всю цепочку: от 'A' к 'B',
от 'B' к 'C', и, наконец, от 'C' к 'D'. При этом, ни 'B', ни 'C' объект 'Config' для выполнения
их работы не нужен. Все, что они делают — это «переносят» 'Config' к 'D', который и зависит от 'Config'.
Отсюда и название «перенос зависимостей».
Если вы работали над большой системой, вы, возможно, видели множество случаев переноса зависимостей —
параметров, которые просто передаются на более низкий уровень.
Перенос зависимостей создает много «шума» в коде, делая трудным его чтение и поддержку. К тому же,
это затрудняет тестирование компонентов. Если вызов метода компонента 'A' требует некоторого объекта
'OX' только потому, что он нужен его «коллаборатору» 'CY' (По определению, коллабораторы - это классы,
которые либо зависят от других, либо предоставляют что-либо другому классу. Получается, что категория
«коллаборатор» объединяет в себе понятия «зависимый класс» и «зависимость», является их надмножеством),
вам все равно нужно предоставить экземпляр 'OX' при тестировании метода объекта 'A', даже если он его
не использует.
Даже если вы используете mock-реализацию коллаборатора 'CY', который может не использовать объект 'OX'.
Вы можете обойти это, передав null вместо 'OX', если в тестируемом методе нет проверки на null. Иногда
в ходе теста может быть сложно создать объект 'OX'. Если конструктор 'OX' зависит от множества других
объектов или значений, вашему тесту также придется передавать осмысленные объекты/значения для этих
параметров. И если 'OX' зависит от 'OY', который зависит от 'OZ', это становится настоящим безумием.
Когда стеки вызова глубоки, перенос зависимостей — это настоящая боль. Особенно, если вы обнаруживаете,
что компонент со дна стека нуждается в другом объекте, доступном выше по стеку. Затем вам придется
добавить этот объект как параметр во все вызовы методов вниз по стеку, начиная тем, откуда требуемый
объект доступен и заканчивая тем, где он необходим.
Общее решение для проблемы «переноса зависимостей» — сделать необходимые объекты статическими синглтонами.
Таким образом любой компонент системы сможет получить доступ к синглтону через его статический фабричный
метод (не путать с паттерном Фабричный Метод). К сожалению, статические синглтоны тянут за собой целый
ворох других проблем, в которые мы здесь не будем погружаться.
Статические синглтоны — зло. Не используйте их, если вам удастся избежать этого.
Когда вы используете DI-контейнер, вы можете снизить «перенос зависимостей» и сократить использование
статических синглтонов. Контейнер знает обо всех компонентах в приложении. Следовательно, он может
идеально связать компоненты, без необходимости передавать зависимости одному компоненту через другой.
Пример с компонентами при использовании контейнера будет выглядеть следующим образом:
****************************************************************************************************
Контейнер создает Config
Контейнер создает D и внедряет Config
Контейнер создает C и внедряет D
Контейнер создает B и внедряет C
Контейнер создает A и внедряет B
A --> B --> C --> D --> Config
****************************************************************************************************
Когда 'A' вызывает 'B', ему не нужно передавать объект 'Config' в 'B'. 'D' уже знает об объекте 'Config'.
Однако, все еще могут быть ситуации, когда нельзя избежать переноса зависимостей. Например, если ваше
приложение обрабатывает запросы (это веб-приложение или веб-сервис), и компоненты, обрабатывающие
запросы это — синглтоны. Тогда объект запроса может быть необходимо передавать вниз по цепочке вызовов
каждый раз, когда доступ к нему понадобится компоненту более низкоуровневого слоя.