Статьи

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

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

Эта концепция может быть расширена во многих отношениях. Во-первых, добавив поддержку подтипа (теперь будут работать только точные совпадения типов). Кроме того, есть возможность определять схемы не только в классе, но и в методах; или расширив поиск до смешанных признаков, чтобы можно было разделить определения 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(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?

Ссылка: Внедрение зависимостей с помощью макросов Scala: автоматическое подключение от нашего партнера JCG Адама Варски в блоге Блог Адама Варски .