Как реализовать dependency injection в Scala?
В Scala существует несколько способов реализовать dependency injection (DI), включая явное внедрение через параметры конструктора, использование self-type, cake pattern, implicit-параметры, макросы, DI-фреймворки (например, MacWire, Guice, Scaldi) и Tagless Final подход. Scala не имеет встроенного DI-контейнера, как Spring в Java, поэтому разработчики выбирают подход исходя из архитектуры проекта.
1. Внедрение зависимостей через параметры конструктора (Constructor Injection)
Самый простой и предпочтительный способ, особенно в функциональном стиле:
trait Database {
def query(sql: String): List\[String\]
}
class UserService(db: Database) {
def getUsers(): List\[String\] = db.query("SELECT \* FROM users")
}
class ProdDatabase extends Database {
def query(sql: String) = List("user1", "user2")
}
val service = new UserService(new ProdDatabase)
Преимущества:
-
Явные зависимости
-
Хорошо работает с тестированием
-
Простая реализация
2. Self-type и cake pattern
Использование self-type позволяет описать зависимости на уровне типов. Cake pattern строится на этом механизме.
trait DatabaseComponent {
trait Database {
def query(sql: String): List\[String\]
}
val database: Database
}
trait UserServiceComponent {
self: DatabaseComponent =>
class UserService {
def getUsers(): List\[String\] = database.query("SELECT \* FROM users")
}
val userService: UserService
}
Композиция:
object AppComponents extends UserServiceComponent with DatabaseComponent {
val database = new Database {
def query(sql: String) = List("userA", "userB")
}
val userService = new UserService
}
Cake pattern позволяет декомпозировать зависимости и строить модули, но может быть сложным в сопровождении.
3. Implicits (неявные зависимости)
Можно передавать зависимости неявно через implicit параметры:
trait Logger {
def log(msg: String): Unit
}
class ConsoleLogger extends Logger {
def log(msg: String): Unit = println(s"LOG: $msg")
}
class Service(implicit logger: Logger) {
def run(): Unit = logger.log("Service started")
}
implicit val logger = new ConsoleLogger
val service = new Service
service.run()
Плюсы:
-
Уменьшение шаблонного кода
-
Хорошо сочетается с DSL и функциональным стилем
Минусы:
-
Неочевидные зависимости
-
Сложнее отлаживать
4. Dependency Injection с помощью MacWire
MacWire — библиотека, генерирующая внедрение зависимостей во время компиляции, без рефлексии. Не требует аннотаций.
import com.softwaremill.macwire._
trait Database {
def query(sql: String): List\[String\]
}
class UserService(db: Database) {
def getUsers(): List\[String\] = db.query("SELECT \* FROM users")
}
class ProdDatabase extends Database {
def query(sql: String) = List("admin", "guest")
}
trait Components {
lazy val db = wire\[ProdDatabase\]
lazy val service = wire\[UserService\]
}
MacWire работает через wire[T], который сопоставляет зависимости по параметрам конструктора. Это компилируется в обычный new.
5. Использование DI-фреймворков, таких как Google Guice
Можно использовать Google Guice в Scala с минимальной обвязкой:
class UserService @Inject() (db: Database) {
def getUsers(): List\[String\] = db.query("SELECT \* FROM users")
}
class ProdModule extends AbstractModule {
def configure(): Unit = {
bind(classOf\[Database\]).to(classOf\[ProdDatabase\])
}
}
Инициализация:
val injector = Guice.createInjector(new ProdModule())
val service = injector.getInstance(classOf\[UserService\])
Минус — необходимость аннотаций и зависимость от Java-библиотеки.
6. Scaldi (DI-фреймворк для Scala)
Scaldi — фреймворк DI, написанный на Scala и использующий DSL:
class MyModule extends Module {
bind\[Database\] to new ProdDatabase
bind\[UserService\] to injected\[UserService\]
}
implicit val injector = new MyModule
val service = inject\[UserService\]
Scaldi использует inject[T], чтобы извлекать зависимости. Более естественно вписывается в Scala-код, чем Guice.
7. Tagless Final
Используется в функциональном программировании как альтернатива DI.
trait Console\[F\[\_\]\] {
def putStrLn(s: String): F\[Unit\]
}
class Program\[F\[\_\]: Console\] {
def run(): F\[Unit\] = implicitly\[Console\[F\]\].putStrLn("Hello")
}
Инстанс можно подставить для любой монады (IO, Future, Id). Это работает как DI без DI-контейнера, при этом зависимости доступны через типы.
8. Manual wiring (ручная сборка)
Можно просто собрать зависимости вручную:
val db = new ProdDatabase
val userService = new UserService(db)
val app = new App(userService)
Этот способ подходит для небольших приложений, где нет необходимости в гибкой архитектуре.
Сравнение подходов
Подход | Гибкость | Явность | Простой для новичков | Поддержка тестов | Производительность |
---|---|---|---|---|---|
Конструктор | ★★★★☆ | ★★★★★ | ★★★★★ | ★★★★★ | ★★★★★ |
--- | --- | --- | --- | --- | --- |
Self-type / Cake Pattern | ★★★★☆ | ★★★★☆ | ★★☆☆☆ | ★★★★☆ | ★★★★★ |
--- | --- | --- | --- | --- | --- |
Implicit | ★★★★☆ | ★★☆☆☆ | ★★★☆☆ | ★★★★☆ | ★★★★★ |
--- | --- | --- | --- | --- | --- |
MacWire | ★★★★★ | ★★★★☆ | ★★★★☆ | ★★★★☆ | ★★★★★ |
--- | --- | --- | --- | --- | --- |
Guice / Scaldi | ★★★☆☆ | ★★★☆☆ | ★★★☆☆ | ★★★☆☆ | ★★★☆☆ |
--- | --- | --- | --- | --- | --- |
Tagless Final | ★★★★★ | ★★★★★ | ★☆☆☆☆ | ★★★★★ | ★★★★★ |
--- | --- | --- | --- | --- | --- |
Выбор зависит от стиля (FP или ООП), размера проекта и предпочтений команды.