Dependency Injection з перевіркою коректності на Scala засобами мови

Хочу розповісти про свою невелику бібліотеку Dependency Injection на Scala. Проблема яку хотілося вирішити: можливість протестувати граф залежностей до їх реального конструювання і падати як можна раніше якщо щось пішло не так, а також бачити в чому саме помилка. Це саме те, чого не вистачає в чудовій DI-бібліотеці Scaldi. При цьому хотілося зберегти зовнішню прозорість синтаксису і максимально обійтися засобами мови, а не ускладнювати і влазити в макрос.
Також хочу відразу звернути увагу що я концентруюся на DI через конструктор, як на самому простому і идиоматичном спосіб, що не вимагає змін в реалізацію класів.
Передати конструктор як функцію в Scala можна за допомогою часткового виклику, наприклад:
class A(p0: Int, p1: Int)
Module().bind(new A(_: Int, _: Int))

Запис досить громіздка, можливо краще буде використовувати наперед визначені функції, що викликають конструктор, які можна передавати явно:
class A(p0: Int, p1: Int)
object A {
def getInstance(p0: Int, p1: Int) = new A(p0, p1)
}
Module().bind(A. getInstance)

Читаність такого стилю помітно краще, так що в прикладах нижче постараюся використовувати саме його.
Приклад
build.sbt:
libraryDependencies += "io.ics" %% "послідовника" % "1.2.1"

Imports:
import io.ics.послідовник._

Припустимо у нас є певний набір класів, примірники яких потрібно впровадити один в одного:
Доменна сутність "Користувач"
case class User(name: String)

Сервіс, що приймає в якості параметра примірник користувача-адміна, має один метод з примітивною реалізацією
class UserService(val admin: User) {
def getUser(name: String) = User(name)
}

Ми хочемо мати примірник цього сервісу у вигляді сінглтона — для того щоб переконатися в тому, що він створюється один раз, заведемо статичний лічильник екземплярів цього класу (для наочності забудемо зараз про багатопотоковому виконання).
object UserService {
var isCreated: Boolean = false
def getInstance(admin: User) = {
isCreated = true
new UserService(admin)
}
}

А також умовний контролер, залежний від цього сервісу
class UserController(service: UserService) {
def renderUser(name: String): String = {
val user = service.getUser(name)
s"User is $user"
}
}

object UserController {
def getInstance(service: UserService) = new UserController(service)
}

Типи біндінгів
За замовчуванням усі наші биндинги ліниві — екземпляри класів створюються тільки за потребою і створюються стільки разів, скільки запитуються.
Ми також можемо додати до оголошення метод
singleton
— в цьому випадку компонент буде створено строго один раз. Метод nonLazy позначає биндинг як неледачий, що означає що цей компонент буде створено при виклику методу
build()
у модуля. Неленивыми можуть бути тільки компоненти-синглтоны.
Подивимося як буде виглядати створення графа залежностей за допомогою бібліотеки Послідовника (зверніть увагу що порядок оголошення біндінгів не важливий):
val depGraph = Module().
// Биндинг контролера, оголошуємо його як singleton
bind(UserController.getInstance _).singleton.
// Биндинг сервісу, відзначаємо його залежність буде прив'язана не тільки за типом, але і за ідентифікатором,
// помічаємо биндинг як nonLazy, що означає, що його примірник буде створено при виклику build().
forNames('admin).bind(UserService.getInstance).singleton.nonLazy.
// Компонент адмінської облікового запису з ідентифікатором 'admin
bind(User("Admin")).byName('admin).
// Компонент облікового запису користувача, доступною за ідентифікатором 'customer. Зверніть увагу що
// два останніх компонента мають один і той же тип і не могли б бути ідентифіковані тільки за нього
bind(User("Jack")).byName('customer).
// Перевіряємо структуру і будуємо кінцевий граф залежностей
build()

Зауваження 1: Ви напевно звернули увагу, що в разі контролера ми форсим передачу параметра функції, а в разі сервісу — немає. Так відбувається в зв'язку з тим, що існує перевантаження функції bind для by-name функції без аргументів, у зв'язку з чим компілятор не може зрозуміти як трактувати функцію без параметрів — як об'єкт або як функцію. Буду радий якщо хтось підкаже як виправити цю дрібну неконсистентность.
Використання:
assert(UserService.isCreated) // Перевіряємо що сервіс був створений одразу після виклику build()

println(depGraph[User]('customer)) // Инжектим компонент типу User з ім'ям customer
println(depGraph[UserService].admin) // Перевіряємо що в сервіс була впроваджена админская учетка
println(depGraph[UserController].renderUser("George")) // Перевіряємо що контролер повертає рядок George

Зауваження 2: якщо один аргумент потрібно отримати по імені, а інший за типом, то можна використовувати оператор
*
:
case class A(label: String)
case class B(a: a label: String)
case class C(a: A, b: B, label: String)

val depGraph = Module().
forNames('labelA).bind { A }.
forNames(*, 'labelB).bind { B }.
forNames(*, *, 'labelC).bind { C }.
bind("instanceA").byName('labelA).
bind("instanceB").byName('labelB).
bind("instanceC").byName('labelC).
build()

Граничні умови
Неповний набір залежностей
val depGraph = Module().
bind {
A("instanceA")
}.
bind {
C (_:, _: B "instanceC")
}.
build()

У цьому прикладі буде викинуто виняток: IllegalStateException: Not found binding for {Type[io.ics.послідовник.B]}. (можливо буде краще створити ієрархію винятків, замість використання скрізь IllegalStateException, але поки до цього не дійшли руки)
Виявлення циклічної залежності
case class Dep1(label: String, d: Dep2)
case class Dep2(d: DepCycle)
case class DepCycle(d: Dep1)

Module().
bind(Dep1("test", _: Dep2)).
bind(Dep2).
bind(DepCycle).
build()

Тут буде викинуто виняток: IllegalStateException: Dependency graph contains cyclic dependency: ( {Type[io.ics.послідовник.DepCycle]} -> {Type[io.ics.послідовник.Dep1]} -> {Type[io.ics.послідовник.Dep2]} -> {Type[io.ics.послідовник.DepCycle]} )
Полиморфические биндинги
За замовчуванням компоненти зв'язуються з кінцевим типами результатів функцій, переданих в
bind()
, але часто це не зовсім те поведінка, якого ми хочемо, наприклад, якщо нам потрібно забиндить компонент трейту:
trait Service

class ServiceImpl extends Service

val depGraph =
Module().
bind(new ServiceImpl(): Service).
build()

Під капотом
Постараюся передати загальну концепцію, не вдаючись у деталі реалізації. Викликаючи метод
.bind()
ми формуємо список пар
(DepId, List[Dep])
,
DepId
це або опис типу результату, яких воно ж + ідентифікатор:
sealed trait DepId

case class TTId(tpe: Type) extends DepId {
override def toString: String = s"{Type[$tpe]}"
}

case class NamedId(name: Symbol, tpe: Type) extends DepId {
override def toString: String = s"{Name[${name.name}], Type[$tpe]}"
}

a Dep являє собою пару обгорнуту функцію-конструктор (Injector) для залежності + список ID, компонент, від яких вона залежить сама:
case class Dep[R](f: Injector[R], depIds: List[DepId])

Як часто доводиться робити в Scala, для того щоб зробити перевантаження методу для різної кількості аргументів, доводиться генерувати бойлерплейты. Одне з таких місць — метод
bind()
. Але, на щастя, плагін sbt-boilerplate робить це заняття трохи менш сумним. Ви просто вводите повторювані оголошення " між квадратними дужками і гратами, і плагін розуміє що їх потрібно повторити, при цьому замінює вся одиниці на n, двійки на n+1 і т. д. BindBoilerplate.scala.template. У підсумку шаблон виходить компактним і усувається необхідність підтримувати ці величезні простирадла вручну.
При виклику методу
build()
список залежностей перетворюється в граф (тобто в мапуа DepId -> Dep), перевіряється на повноту і відсутність циклічних залежностей за допомогою алгоритму DFS, складність якого оцінюється в O(V + E), де V — кількість компонент, E — кількість залежностей між ними. Якщо щось йде не так, викидається виняток, інакше повертається об'єкт класу DepGraph, який вже можна використовувати для отримання кінцевого компонента:
depGraph[T]
або
depGraph[T]('Id)
— якщо нам потрібно отримати іменований компонент.
Я використав саме Symbol, а не String для імен компонентів оскільки це візуально відразу відрізняє ідентифікатори від звичайних рядкових констант в коді. Плюс, на додачу ми отримуємо примусове інтернування, що може бути в цьому випадку корисно.
Більш сухе, але докладний і насичене прикладами опис, а також вихідний код тут
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.