Вы можете рассматривать внедрение зависимостей как причудливое имя для передачи параметров в функцию (или аргументы конструктора для конструктора). Однако обычно DI-контейнеры делают гораздо больше. Среди прочего, одна очень приятная особенность — автоматическое подключение : создание правильных объектов с правильными аргументами. Самые популярные фреймворки ( Spring , Guice , CDI / Weld ) выполняют эту задачу во время выполнения, используя рефлексию.
[rant] У
проводки во время выполнения с отражением есть свои минусы. Во-первых, во время компиляции не проверяется , удовлетворена ли каждая зависимость. Во-вторых, мы теряем некоторую гибкость, которая была бы у нас при работе вручную, поскольку мы должны соблюдать правила, по которым объекты создаются «автоматически». Например, если по какой-то причине объект необходимо создать вручную, для этого требуется уровень косвенности (шаблон), а именно фабрика. Наконец, часто внедрение зависимостей является «глобальным», то есть существует один контейнер со всеми объектами, трудно создавать локальные / параметризованные «вселенные» (Guice здесь исключение). Наконец-то, наконец, некоторые фреймворки выполняют сканирование путей, который медленный, а иногда может дать неожиданные результаты.
[/ напыщенная]
Слишком волшебно для такой простой вещи.
Но разве мы не хотим просто иметь способ new
сгенерировать все правильные параметры для нас? Если вы используете Scala и хотите генерацию кода, очевидный ответ — макросы !
Чтобы наконец показать некоторый код, учитывая:
class A class B class C(a: A, b: B) class D(b: B, c: C)
было бы неплохо иметь:
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]
преобразовано в:
val a = new A() val theB = new B() val theC = new C(a, theB) val d = new D(theB, c)
Оказывается, это возможно, и даже не очень сложно.
Подтверждение концепции доступно на GitHub . Это очень примитивно и в настоящее время поддерживает только один конкретный способ определения классов / связей, но работает :). Если зависимость отсутствует, возникает ошибка компиляции. Чтобы проверить это, просто клонируйте репозиторий, запустите sbt
и затем вызовите задачу: run-main com.softwaremill.di.DiExampleRunner
( реализация ). Во время компиляции вы должны увидеть некоторые информационные сообщения, касающиеся сгенерированного кода, например:
[info] /Users/adamw/(...)/DiExample.scala:13: Generated code: new C(a, theB) [info] val c = wire[C] [info] ^
и затем доказательство того, что код действительно был сгенерирован правильно: когда код выполняется, экземпляры печатаются в стандартный вывод, чтобы вы могли видеть аргументы.
Макрос здесь, конечно, wire
метод ( реализация ). Он сначала проверяет, каковы параметры конструктора класса, а затем для каждого параметра пытается найти val
определенный во включающем классе требуемый тип ( findWiredOfType
метод; см. Также этот вопрос StackOverflow, почему поиск ограничен к вмещающему классу). Наконец, он собирает дерево, соответствующее вызову конструктора с правильными аргументами:
Apply( Select(New(Ident([class's type])), nme.CONSTRUCTOR), List(Ident([arg1]), Ident([arg2]), ...))
Эта концепция может быть расширена во многих отношениях. Во-первых, добавив поддержку подтипа (теперь будут работать только точные совпадения типов). Кроме того, есть возможность определять схемы не только в классе, но и в методах; или расширение поиска до смешанных признаков, чтобы можно было разделить wire
определения между несколькими признаками («модулями»?). Обратите внимание, что мы также можем иметь полную гибкость в том, как мы получаем доступ к проводной сети; это может быть val
, lazy val
или def
. Также есть поддержка областей видимости, фабрик, синглетонов, значений конфигурации,…; например:
( Внедрение зависимости будущего! )
// "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(p1: T, p2: 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?