Вы можете рассматривать внедрение зависимостей как причудливое имя для передачи параметров в функцию (или аргументы конструктора для конструктора). Однако обычно DI-контейнеры делают гораздо больше. Среди прочего, одна очень приятная особенность — автоматическое подключение : создание правильных объектов с правильными аргументами. Самые популярные фреймворки ( Spring , Guice , CDI / Weld ) выполняют эту задачу во время выполнения, используя рефлексию.
[rant] У проводки во время выполнения с отражением есть свои минусы. Во-первых, во время компиляции не проверяется , удовлетворена ли каждая зависимость. Во-вторых, мы теряем некоторую гибкость, которая была бы у нас при работе вручную, поскольку мы должны соблюдать правила, по которым объекты создаются «автоматически». Например, если по какой-то причине
объект должен быть создан вручную, для этого необходим уровень косвенности (шаблон), а именно фабрика. Наконец, часто внедрение зависимостей является «глобальным», то есть существует один контейнер со всеми объектами, трудно создавать локальные / параметризованные «вселенные» (Guice здесь исключение). Наконец-то, наконец, некоторые фреймворки выполняют сканирование путей к классам , которое медленное и иногда может дать неожиданные результаты.
[/ Напыщенная]
Слишком волшебно для такой простой вещи. Но разве мы не хотим, чтобы у нас были new
с правильными параметрами? Если вы используете Scala и хотите генерацию кода, очевидный ответ — макросы !
Чтобы наконец показать некоторый код, учитывая:
1
2
3
4
|
class A class B class C(a : A, b : B) class D(b : B, c : C) |
было бы неплохо иметь:
1
2
3
4
|
val a = wire[A] val theB = wire[B] // 'theB', not 'b', just to show that we can use any name val theC = wire[C] val d = wire[D] |
преобразовано в:
1
2
3
4
|
val a = new A() val theB = new B() val theC = new C(a, theB) val d = new D(theB, c) |
Оказывается, это возможно, и даже не очень сложно. Подтверждение концепции доступно на GitHub . Это очень примитивно и в настоящее время поддерживает только один конкретный способ определения классов / связей, но работает. Если зависимость отсутствует, возникает ошибка компиляции. Чтобы проверить это, просто sbt
, запустите sbt
и затем вызовите задачу: run-main com.softwaremill.di.DiExampleRunner
( реализация ). Во время компиляции вы должны увидеть некоторые информационные сообщения, касающиеся сгенерированного кода, например:
1
2
3
|
[info] /Users/adamw/ (...) /DiExample .scala:13: Generated code: new C(a, theB) [info] val c = wire[C] [info] ^ |
и затем доказательство того, что код действительно был сгенерирован правильно: когда код выполняется, экземпляры печатаются в стандартный вывод, чтобы вы могли видеть аргументы. Макрос здесь, конечно, является wire
методом ( реализацией ). Он сначала проверяет, каковы параметры конструктора класса, а затем для каждого параметра пытается найти значение val
определенное во включающем классе нужного типа (метод findWiredOfType
; см. Также этот вопрос StackOverflow, почему поиск ограничено классом ограждения). Наконец, он собирает дерево, соответствующее вызову конструктора с правильными аргументами:
1
2
3
|
Apply( Select(New(Ident([ class 's type ])), nme.CONSTRUCTOR), List(Ident([arg 1 ]), Ident([arg 2 ]), ...)) |
Эта концепция может быть расширена во многих отношениях. Во-первых, добавив поддержку подтипа (теперь будут работать только точные совпадения типов). Кроме того, есть возможность определять схемы не только в классе, но и в методах; или расширив поиск до смешанных признаков, чтобы можно было разделить определения wire
между несколькими признаками («модулями»?). Обратите внимание, что мы также можем иметь полную гибкость в том, как мы получаем доступ к проводной сети; это может быть val
, lazy val
или def
. Также есть поддержка областей видимости, фабрик, синглетонов, значений конфигурации,…; например:
( Внедрение зависимости будущего! )
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
// 'scopes' val a = wire[X] lazy val b = wire[Y] def c = wire[Z] val d = provided(manuallyCreatedInstance) // override a single dependency val a = wire[X]. with (anotherYInstance) // factories: p1, p2, ... are used in the constructor where needed def e(p 1 : T, p 2 : U, ...) = wire[X] // by-name binding for configuration parameters; whenever a class has a // 'maxConnections' constructor argument, this value is used. val maxConnections = conf( 10 ) |
Недавний проект создателя Guice Боба Ли идет в том же направлении. Насколько мне известно, Dagger (в основном ориентированный на Android) использует процессор аннотаций для генерации кода проводки; во время выполнения это просто вызовы конструктора, без отражения. Точно так же и здесь, с той разницей, что мы используем макросы Scala.
Что вы думаете о таком подходе к DI?
Ссылка: Внедрение зависимостей с помощью макросов Scala: автоматическое подключение от нашего партнера JCG Адама Варски в блоге Блог Адама Варски .