Статьи

MacWire 0.1: внедрение зависимостей без рамок с макросами Scala

Использование Dependency Injection является почти стандартом при разработке программного обеспечения. Тем не менее, во многих случаях может показаться, что использование шаблона подразумевает использование контейнера / инфраструктуры DI. Но действительно ли нужны рамки?

Для реализации DI все, что вам действительно нужно, это удалить news из вашего кода и перенести зависимости в конструктор. Конечно, должно быть какое-то место, где создаются объекты; для этого вам нужен верхний уровень («основной» класс), где вся проводка сделана. Вот где DI-контейнеры действительно помогают: они снимают утомительную задачу передачи правильных параметров конструкторам. Обычно это делается во время выполнения с помощью отражения.

MacWire использует другой подход. Основываясь на объявлениях, указывающих, какие классы должны быть созданы, он генерирует код, необходимый для создания нового экземпляра класса, с правильными параметрами, взятыми из включающего типа. Это делается во время компиляции с использованием макроса Scala . Затем код проверяется типом с помощью компилятора Scala, поэтому весь процесс является типобезопасным, и если зависимость отсутствует, вы сразу об этом узнаете (в отличие от традиционных контейнеров DI).

Например, учитывая:

class DatabaseAccess()
class SecurityFilter()
class UserFinder(databaseAccess: DatabaseAccess, securityFilter: SecurityFilter)
class UserStatusReader(userFinder: UserFinder)
 
trait UserModule {
    import com.softwaremill.macwire.MacwireMacros._
 
    lazy val theDatabaseAccess   = wire[DatabaseAccess]
    lazy val theSecurityFilter   = wire[SecurityFilter]
    lazy val theUserFinder       = wire[UserFinder]
    lazy val theUserStatusReader = wire[UserStatusReader]
}

Сгенерированный код будет:

trait UserModule {
    lazy val theDatabaseAccess   = new DatabaseAccess()
    lazy val theSecurityFilter   = new SecurityFilter()
    lazy val theUserFinder       = new UserFinder(theDatabaseAccess, theSecurityFilter)
    lazy val theUserStatusReader = new UserStatusReader(theUserFinder)
}

Классы , которые должны быть подключены должны содержаться в Scala trait, classили object(контейнер образует «модуль»). MacWire ищет значения по включающему типу (признак / класс / объект) и по любым супер-признакам / классам. Следовательно, возможно объединение нескольких модулей с использованием наследования.

В настоящее время поддерживаются две области; зависимость может быть одноэлементной (объявленной как val/ lazy val), или для каждого использования может быть создан новый экземпляр (объявленный как def).

Обратите внимание, что этот подход очень гибкий; все, с чем мы здесь имеем дело, это обычный код Scala, поэтому, если класс нужно создать каким-то особым образом, ничто не мешает нам просто записать его в виде кода. Кроме того, wire[T]может быть вложен в тело метода, и он будет расширен и до создания нового экземпляра.

Для интеграционного тестирования модуля, если для некоторых классов мы хотели бы использовать макет, достаточно простого переопределения, например:

trait UserModuleForTests extends UserModule {
    override lazy val theDatabaseAccess = mockDatabaseAccess
    override lazy val theSecurityFilter = mockSecurityFilter
}

Проект является продолжением моего предыдущего поста в блоге . Существует также аналогичный проект для Java, который использует процессоры аннотаций: Dagger .

MacWire доступен в Maven Central . Чтобы использовать, просто добавьте эту строку в вашу сборку SBT:

libraryDependencies += "com.softwaremill.macwire" %% "core" % "0.1"

Код на GitHub , под лицензией Apache2, так что не стесняйтесь использовать, вилку и исследовать. Взгляните на README, которая содержит больше информации.

Планы на будущее включают поддержку фабрик и поиск параметров по именам, поддержку значений конфигурации и других областей, таких как области запросов или сессий, которые очень полезны для веб-проектов.