Цитата Dependency Injection Демистифицирована :
«Внедрение зависимостей» — это понятие за 5 центов стоимостью 25 долларов.
* Джеймс Шор, 22 марта 2006 г.
Внедрение зависимостей, насколько это важно при написании тестируемых, компонуемых и хорошо структурированных приложений, означает не что иное, как наличие объектов с конструкторами. В этой статье я хочу показать вам, как внедрение зависимостей является просто синтаксическим сахаром, который скрывает функции каррирования и композиции. Не волнуйтесь, мы очень медленно попытаемся объяснить, почему эти два понятия очень похожи.
Сеттеры, аннотации и конструкторы
Spring bean или EJB — это объект Java. Однако, если вы посмотрите внимательно, большинство бинов фактически не сохраняют состояния после создания. Вызов методов в bean-компоненте Spring редко изменяет состояние этого bean-компонента. В большинстве случаев bean-компоненты — это просто удобные пространства имен для множества процедур, работающих в аналогичном контексте. Мы не изменяем состояние CustomerService
при вызове invoice()
, мы просто делегируем другому объекту, который в конечном итоге вызовет базу данных или веб-сервис. Это уже далеко от объектно-ориентированного программирования (о чем я здесь говорил). Таким образом, по сути, у нас есть процедуры (мы перейдем к функциям позже) в многоуровневой иерархии пространств имен: пакеты и классы, к которым они принадлежат. Обычно эти процедуры вызывают другие процедуры. Вы можете сказать, что они вызывают методы для зависимостей bean-компонентов, но мы уже узнали, что bean-компоненты — ложь, это всего лишь группы процедур.
При этом давайте посмотрим, как вы можете настроить bean-компоненты. В моей карьере у меня были эпизоды с сеттерами (и тоннами <property name="...">
в XML), @Autowired
на полях и, наконец, внедрение в конструктор. Читайте также: Почему инъекция по конструктору должна быть предпочтительной? , Так что у нас обычно есть объект, который имеет неизменные ссылки на свои зависимости:
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
|
Hello Rajeev, @Component class PaymentProcessor { private final Parser parser; private final Storage storage; @Autowired public PaymentProcessor(Parser parser, Storage storage) { this .parser = parser; this .storage = storage; } void importFile(Path statementFile) throws IOException { try (Stream<string> lines = Files.lines(statementFile)) { lines .map(parser::toPayment) .forEach(storage::save); } } } @Component class Parser { Payment toPayment(String line) { //om-nom-nom... } } @Component class Storage { private final Database database; @Autowired public Storage(Database database) { this .database = database; } public UUID save(Payment payment) { return this .database.insert(payment); } } class Payment { //... }</string> |
Возьмите файл с банковскими выписками, проанализируйте каждую отдельную строку в объекте Payment
и сохраните его. Как скучно, как вы можете получить. Теперь давайте немного проведем рефакторинг. Прежде всего, я надеюсь, вы знаете, что объектно-ориентированное программирование — это ложь. Не потому, что это просто набор процедур в пространствах имен так называемых классов (надеюсь, вы не пишете программное обеспечение таким образом). Но поскольку объекты реализованы как процедуры с неявным параметром this
, когда вы видите: this.database.insert(payment)
он фактически скомпилирован во что-то вроде этого: Database.insert(this.database, payment)
. Не веришь мне?
1
2
3
4
5
6
7
8
9
|
$ javap -c Storage. class ... public java.util.UUID save(com.nurkiewicz.di.Payment); Code: 0 : aload_0 1 : getfield # 2 // Field database:Lcom/nurkiewicz/di/Database; 4 : aload_1 5 : invokevirtual # 3 // Method com/nurkiewicz/di/Database.insert:(Lcom/nurkiewicz/di/Payment;)Ljava/util/UUID; 8 : areturn |
Хорошо, если вы нормальный человек, это не доказательство для вас, поэтому позвольте мне объяснить. aload_0
(представляющий this
) с последующим getfield #2
this.database
в стек операндов. aload_1
первый параметр метода ( Payment
) и, наконец, вызывает invokevirtual
процедуры Database.insert
(здесь присутствует некоторый полиморфизм, не относящийся к данному контексту). Таким образом, мы фактически вызвали двухпараметрическую процедуру, в которой первый параметр автоматически заполнялся компилятором и назывался… this
. На стороне вызываемого this
допустимо и указывает на экземпляр Database
.
Забудь об объектах
Давайте сделаем все это более явным и забудем об объектах:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
class ImportDependencies { public final Parser parser; public final Storage storage; //... } static void importFile(ImportDependencies thiz, Path statementFile) throws IOException { Files.lines(statementFile) .map(thiz.parser::toPayment) .forEach(thiz.storage::save); } |
Это безумие! Обратите внимание, что процедура importFile
теперь находится за пределами PaymentProcessor
, который я фактически переименовал в ImportDependencies
(простите за public
модификатор для полей). importFile
может быть static
потому что все зависимости явно указаны в thiz
контейнере, не подразумеваются с помощью переменных this
и instance — и могут быть реализованы где угодно. На самом деле мы просто переделали то, что уже происходит за кулисами во время компиляции. На этом этапе вы можете задаться вопросом, зачем нам нужен дополнительный контейнер для зависимостей, а не просто передавать их напрямую. Конечно, это бессмысленно
1
2
3
4
5
|
static void importFile(Parser parser, Storage storage, Path statementFile) throws IOException { Files.lines(statementFile) .map(parser::toPayment) .forEach(storage::save); } |
На самом деле, некоторые люди предпочитают передавать зависимости явно бизнес-методам, как описано выше, но это не главное. Это просто еще один шаг в трансформации.
Карринг
Для следующего шага нам нужно переписать нашу функцию в Scala:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
object PaymentProcessor { def importFile(parser: Parser, storage: Storage, statementFile: Path) { val source = scala.io.Source.fromFile(statementFile.toFile) try { source.getLines() .map(parser.toPayment) .foreach(storage.save) } finally { source.close() } } } |
Это функционально эквивалентно, так что не так много, чтобы сказать. Просто обратите внимание на то, как importFile()
принадлежит object
, так что он чем-то похож на static
методы в singleton в Java. Далее мы сгруппируем параметры :
1
|
def importFile(parser: Parser, storage: Storage)(statementFile: Path) { //... |
Это имеет все значение. Теперь вы можете указать все зависимости постоянно или лучше, сделайте это только один раз:
1
2
3
4
5
|
val importFileFun: (Path) => Unit = importFile(parser, storage) //... importFileFun(Paths.get( "/some/path" )) |
Строка выше может фактически быть частью настройки контейнера, где мы связываем все зависимости вместе. После установки мы можем использовать importFileFun
где угодно, не зная о других зависимостях. Все, что у нас есть, это функция (Path) => Unit
, как в paymentProcessor.importFile(path)
в самом начале.
Функции полностью вниз
Мы по-прежнему используем объекты как зависимости, но если вы посмотрите внимательно, нам не нужны ни parser
ни storage
. Что нам действительно нужно, так это функция , которая может анализировать ( parser.toPayment
), и функция, которая может хранить ( storage.save
). Давайте снова проведем рефакторинг:
01
02
03
04
05
06
07
08
09
10
|
def importFile(parserFun: String => Payment, storageFun: Payment => Unit)(statementFile: Path) { val source = scala.io.Source.fromFile(statementFile.toFile) try { source.getLines() .map(parserFun) .foreach(storageFun) } finally { source.close() } } |
Конечно, мы можем сделать то же самое с Java 8 и лямбдами, но синтаксис более многословен. Мы можем предоставить любую функцию для анализа и хранения, например, в тестах мы можем легко создавать заглушки. Да, и кстати, мы только что превратились из объектно-ориентированной Java в функциональную композицию, а не в объекты вообще. Конечно, есть еще побочные эффекты, например, загрузка файла и сохранение, но давайте оставим это так. Или, чтобы сделать сходство между внедрением зависимостей и составом функций еще более поразительным, посмотрите эквивалентную программу на Haskell:
1
2
3
4
5
6
7
|
let parseFun :: String -> Payment let storageFun :: Payment -> IO () let importFile :: (String -> Payment) -> (Payment -> IO ()) -> FilePath -> IO () let simpleImport = importFile parseFun storageFun // :t simpleImport // simpleImport :: FilePath -> IO () |
Прежде всего, монада IO
необходима для управления побочными эффектами. Но видите ли вы, как importFile
высшего порядка importFile
принимает три параметра, но мы можем предоставить только два и получить simpleImport
? Это то, что мы называем внедрением зависимостей в Spring или EJB. Но без синтаксиса сахар.
Ссылка: | Внедрение зависимостей: синтаксический сахар над композицией функций от нашего партнера по JCG Томаша Нуркевича из блога Java и соседей . |