Статьи

Внедрение зависимостей с помощью макросов Scala: автоматическое подключение

Вы можете рассматривать внедрение зависимостей как причудливое имя для передачи параметров в функцию (или аргументы конструктора для конструктора). Однако обычно 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?