Статьи

Инъекция зависимости в игру! с MacWire

Самая последняя версия  MacWire (0.4)  (макрос Scala для генерации связующего кода для создания экземпляров класса, замены контейнера DI) поставляется с новыми утилитами, которые облегчают интеграцию с каркасами, которые требуют  поиска экземпляров по классам . Примером такой структуры является  Play! , который довольно быстро набирает популярность в мире веб-разработки.

Есть два способа узнать, как сделать внедрение без контейнера в Play! (и другие веб-фреймворки) с MacWire:

  1. читать этот блог
  2. попробуйте  MacWire + Play Typesafe Activator

Или вы можете сделать оба;)

МОТИВАЦИЯ

Многие веб-фреймворки требуют некоторой формы общего поиска экземпляров, например, когда на экземпляры контроллера ссылаются из представлений. В традиционных DI-контейнерах, таких как  Guice , это обеспечивается из коробки, так как это основной способ получения проводных объектов. Например, в Guice у нас есть   объект, Injector который переводит  Classобъекты в экземпляры данного класса.

При использовании MacWire (или при «ручном» DI) мы делаем максимально возможный типобезопасный способ, просто ссылаясь на объекты как поля / методы в объекте-контейнере, поэтому нам обычно не нужны такие карты. Но для интеграции с веб-фреймворком это все еще необходимо.

ОБЩИЕ УТИЛИТЫ

MacWire 0.4 поставляется с двумя общими утилитами, полезными для поиска экземпляров по классам.

Первый — это макрос,  valsByClass(object) который генерирует (конечно же, во время компиляции) карту vals в данном объекте с ключами их классов. Так, например:

object Example1 {
   val aString = "Hello World!"
   val userAuthenticator = new UserAuthenticator()
   // here we are using the core MacWire feature: the wire macro
   val userFinder = wire[UserFinder] 
}
 
val theMap = valsByClass(Example1)

переведет следующий код:

val theMap = Map[Class[_], AnyRef](
   classOf[String] -> Example1.aString,
   classOf[UserAuthenticator] -> Example1.userAuthenticator,
   classOf[UserFinder] -> Example1.userFinder
)

Вторая утилита  InstanceLookupрасширяет эту карту, чтобы разрешить поиск по подклассам / признакам. Например, скажем, у нас есть  Database черта и реализация,  MySQLDatabaseкоторая заканчивается на карте vals-by-class под  Class[MySQLDatabase] ключом.

Тем не менее, мы все равно хотели бы получить экземпляр при запросе  Database. Мы можем достичь этого, обернув карту  InstanceLookup:

trait Database
class MySQLDatabase extends Database
 
object App {
   val database = new MySQLDatabase()
}
 
val instanceLookup = InstanceLookup(valsByClass(App))
 
require(instanceLookup(classOf[Database]) == App.database)

ИГРАТЬ ИНТЕГРАЦИЯ

Имея эти утилиты, мы можем перейти к интеграции MacWire с Play.

Основное изменение заключается в том, что контроллеры теперь будут не  objects, а  classes (обычно с непустыми списками параметров конструктора — зависимости). Теперь задача состоит в том, чтобы сказать Play, как получить значения контроллера?

Допустим, у нас есть следующий контроллер и класс обслуживания:

package controllers
 
import play.api.mvc._
import services.HelloWorldProvider
 
class MainController(helloWorldProvider: HelloWorldProvider) extends Controller {
  def index = Action {
    Ok(views.html.index(helloWorldProvider.text))
  }
}
 
package services
 
class HelloWorldProvider {
   def text = "Hello World!"
}

Здесь контроллер ( MainController) имеет одну зависимость,  HelloWorldProvider сервис.

Теперь, когда у нас есть готовый пример, мы можем приступить к интеграции. Есть три простых шага. Во-первых, нам нужно связать зависимости. Мы можем использовать  wire[] макрос или просто использовать ручной DI:

trait OurApplication {
   lazy val helloWorldProvider = new HelloWorldProvider
   lazy val mainController = new MainController(helloWorldProvider)
}

Во-вторых, нам нужно указать в файле маршрутов, что мы хотим предоставить экземпляры контроллеров самостоятельно, вместо того, чтобы позволить Play создавать экземпляры контроллеров. Это делается путем добавления  @ символа перед именем контроллера:

// File: conf/routes
 
GET     /                           @controllers.MainController.index()

Наконец, нам нужно создать  Global объект в основном пакете. Это главная точка интеграции; здесь мы используем карты поиска экземпляров по классам:

import com.softwaremill.macwire.{InstanceLookup, Macwire}
import play.api.GlobalSettings
 
object Global extends GlobalSettings with Macwire {
  val instanceLookup = InstanceLookup(valsByClass(new OurApplication {}))
 
  override def getControllerInstance[A](controllerClass: Class[A]) = 
      instanceLookup.lookupSingleOrThrow(controllerClass)
}

Вот и все! Отныне экземпляры контроллера будут предоставляться с нашей карты, используя проводку, которую мы определили в  OurApplication характеристике.

Обязательно ознакомьтесь с  Активатором,  если хотите увидеть рабочий пример в действии!

Адам