简介
Play可以被认为是一个灵活的框架,用户不需要遵循设计者设定的的路线.在需要引入依赖注入机制时有很多方式可以选择.默认的解决方案是由Play提供的JSR 330
并由Guice
实现.实时上Play是DI-agnostic(DI的无知论者)
- 可以使用多样的运行时依赖注入,包含蛋糕模式和功能技术,比如reader monad
.或者有些时候需要混入不同的技术.我会尝试描述其中的一些方式,并且其中一些并不是为Play专门提供的,同样可以在一些其他的Scala应用中使用.
样例应用
这节的内容包含了一些对Play应用中依赖注入的描述.他们都提供了一些简单的功能.
样例程序暴漏了三个HTTP端点: 一个返回图书列表,一个通过ID返回一本图书,一个用于更新图书的标题.所有的HTTP请求都通过一个基于BooksService
的BooksController
进行处理,而BooksService
用以提供数据.BooksService
的实现 - CachingBooksService
基于由Play提供的CacheApi
组件.因此,我们来看一下如何来声明各个组件的依赖关系,定义我们自己的可注入组件(BooksService)
然后访问由Play提供的CacheApi
组件.
这个应用非常简单,仅用来展示DI的用法,并没有HTML视图,使用版本为Play 2.5.0和Scala 2.11.
下面首先是不包含DI的代码,所有的依赖仅声明为构造器参数:
case class Book(id: Int, title: String)
object Book {
implicit val jsonFormat = Json.format[Book]
}
trait BooksService {
def list: Seq[Book]
def get(id: Int): Option[Book]
def save(book: Book): Unit
}
class CachingBooksService(cache: CacheApi) extends BooksService {
private val db = mutable.Map(
1 -> Book(1, "Twilight"),
2 -> Book(2, "50 Shades of Grey")) //simulates some persistent storage
override def list: Seq[Book] = {
//get "books" entry from cache, if it doesn't exist fetch fresh list from the "DB"
cache.getOrElse("books") {
def freshBooks = fetchFreshBooks()
cache.set("books", freshBooks, 2.minutes) //cache freshly fetched books for 2 minutes
freshBooks
}
}
override def get(id: Int): Option[Book] = {
cache.getOrElse(s"book$id") {
def freshBook = fetchFreshBook(id)
cache.set(s"book$id", freshBook, 2.minutes)
freshBook
}
}
override def save(book: Book): Unit = {
db(book.id) = book
}
private def fetchFreshBooks(): Seq[Book] = {
db.values.toSeq.sortBy(_.id)
}
private def fetchFreshBook(id: Int): Option[Book] = {
db.get(id)
}
}
class BooksController(booksService: BooksService) extends Controller {
def get(id: Int) = Action {
booksService.get(id).fold(NotFound: Result) { book =>
Ok(Json.toJson(book))
}
}
def list = Action {
def books = booksService.list
Ok(Json.toJson(books))
}
def updateTitle(id: Int) = Action(parse.text) { request =>
booksService.get(id).fold(NotFound: Result) { book =>
val updatedBook = book.copy(title = request.body)
booksService.save(updatedBook)
NoContent
}
}
}
然后是routes
的定义:
GET /books/:id controllers.BooksController.get(id: Int)
GET /books controllers.BooksController.list
POST /books/:id/updateTitle controllers.BooksController.updateTitle(id: Int)
默认的Guice
JSR 330
方式由Play框架之外提供,默认的实现是Guice库.同时作为默认方式和运行时依赖注入,这种技术需要最少的开发者精力来进行配置.框架所提供的所有组件都已准备就绪以用于注入.如果你使用默认的路由器,就无需烦恼于它的初始化,带有控制器(controller)的路由器(router)会自动被注入.并且这种机制不需要创建自定义的载入器(loader)来工作.
定义组件
在组件中不需要明确的指定一个特定的类(class),因此在创建可注入组件时只需要简单的创建一个类.如果你想创建一个组件来实现接口(interface)并使这个接口可注入(injectable)(注:这也是最常用的方式),这时候需要使用@ImplementedBy
注解或者创建一个模块类(module class).
import com.google.inject.ImplementedBy
@ImplementedBy(classOf[ConcreteBooksService])
trait BooksService {
// ...
}
class ConcreteBooksService extends BooksService {
// ...
}
上面例子中的方式需要最少的操作,但是BookService
需要知道实现类ConcreteBooksService
的存在.缺陷在于BookService
可能来自外部的库因此你不能对他进行注解.我的意思是这并不是一个很好的实践,@ImplementedBy
注解并不能适用于所有场景,因为它只能假设定义一个默认的依赖,并能在绑定(bind)的代码中覆写.可以定义一个Guice模块来取代@ImplementedBy
注解:
import com.google.inject.AbstractModule
import play.api.{Configuration, Environment}
class GuiceModule(environment: Environment, configuration: Configuration)
extends AbstractModule {
override def configure() = {
bind(classOf[BooksService]).to(classOf[ConcreteBooksService])
}
}
这里,将ConcreteBooksService
绑定为BooksService
的一个实现.同时可以注意到,GuiceModule
类同时访问了environment
和configuration
两个对象,因此可以根据这两个参数来绑定不同的实现.为了使用这个模块,需要在application.conf
配置文件中使用play.modules.enabled
配置属性首先启动它:
play.modules.enabled += "modules.GuiceModule"
根据组件
使用JSR 330
意味着要使用javax.inject
包的注解.在一些组件中声明依赖时可以注解其类的构造器为@Inject
:
class BooksController @Inject()(booksService: BooksService)
extends Controller {
// ...
}
构造器注入并不是注入依赖的唯一方式,但是setter-injection
注入是一个不好的实践,因此会跳过它.
如之前所说,由Play提供的APIs都可以用来注入,因此可以通过@Inject
来依赖他们而不需要任何其他配置:
class CachingBooksService @Inject()(cache: CacheApi)
extends BooksService {
// ...
}
总结
使用Guice有一些优点: 易于配置,灵活,需要很少的模板代码而且由Play在框架之外提供.最主要的优势是它是一个运行时依赖注入机制,这表示编译器不会发现组件连接问题,除了在应用初始化时,在后续的逻辑执行过程中也可以发现错误.
自己来做:手动依赖注入
依赖注入主要是控制反转 - 你的组件并不复杂构造依赖,而是通过注入获得依赖.你会任何这些很容易实现,因为你并不需要一些指定的结构或库来达到这种效果.如果你的组件类在构造器参数列表中声明依赖,你可以创建一个模块类然后手动的组件所有模块:
class Module {
val cache: CacheApi = new // ...
val booksService: BooksService = new CachingBookService(cache)
val booksController: BooksController = new BooksController(booksService)
}
可以发现,这个Module
类,一旦启动会访问所有的组件,然后这些组件就会完成他们的依赖注入.在应用代码中,通过一个Module
实例来访问所有的组件.DI
模块,或者称为装配工,它本身对于手动DI来说并没有什么不同,它是所有依赖注入机制的概念,只是有些框架对开发者进行了隐藏.
并没有什么特殊的方式来声明一个依赖或组件,任何提供了公共(public)构造器或工厂方法的类,都可以通过在模块中初始化来作为一个组件.所有的Play组件都满足这个条件(像上面所说的,无需配置可以直接根据需要注入).
Play中手动注入
为了使用一个跟Guice不同的方式来使用DI,你需要创建一个自动以的ApplicationLoader
实现.你需要同时连接所有Play自身和你应用中所有需要的组件.Play提供了一个BuiltInComponentsFromContext
抽象类来帮助你实现这种方式:
import controllers.BooksController
import play.api.BuiltInComponents
import play.api.cache.EhCacheComponents
import services.{BooksService, CachingBooksService}
import router.Routes
class ApplicationLoader extends play.api.ApplicationLoader {
def load(context: Context) = new ApplicationModule(context).application
}
class ApplicationModule(context: Context)
extends BuiltInComponentsFromContext(context)
with EhCacheComponents {
lazy val booksService: BooksService = new CachingBooksService(defaultCacheApi)
lazy val booksController = new BooksController(booksService)
lazy val router = new Routes(httpErrorHandler, booksController)
}
上面的例子中,ApplicationModule
类定义了一个DI模块,它继承BuiltInComponentsFromContext
来提供Play自身需要的组件,比如Configuration
和HttpErrorHandler
实例,继承EhCacheComponents
特质来提供BookService
需要的缓存实现.然后构造了BooksService
和BooksController
组件.路由器的实现不能通过BuiltInComponentsFromContext
由Play自动提供,因为他有routes
文本文件生成然后提供给router.Routes
类,因此需要手动构造.因此,所有的依赖必须由你自己手动构造并连接.
你会注意到lazy val
的用法而不是通常的val
,如果只是使用简单的val
定义,在提前使用其他项时会遇到NullPointerExceptions
错误.lazy
定义保证了正确的初始化顺序,因此不需要担心它,但是也是有代价的 - 必须以同步的方式访问他们.
为了使用这个模块,必须定义一个自定义的加载器ApplicationLoader
.为了覆盖Play默认的加载器,必须在application.conf
配置文件中提供play.application.loader
属性并为加载器提供完整的类名.
总结
手动DI的优点主要是编译时的连接正确性验证.相对于Guice方案,你可以通过它在运行应用是感到更安全一点.第二点是不需要任何依赖库.缺陷是需要手动构造并连接所有的组件,这在组件数量很多时会比较令人头痛.在简单应用中,Guice或许过度庞大,因此手动DI也是值得尝试的.
MacWire库 - 宏命令(Macros)
MacWire 是一个提供了Scala宏命令的库,这会使手动DI不那么难用.这个概念和手动DI类似,但是MacWire提供了连接的处理和检查,并且是在编译时进行.这个库不只是提供了一些宏命令,不过本文中仅接受宏命令部分.
简化手动DI
还记得上一节中手动DI的模块定义吗,然我们用MacWire来重写它:
class Module {
import com.softwaremill.macwire._
val cache: CacheApi = // ...
val booksService: BooksService = wire[CachingBookService]
val booksController: BooksController = wire[BooksController]
}
可以发现,构造器调用使用了wire
宏命令进行替换.宏命令来获取依赖,并且和手动ID的代码很相似,甚至是相同.如果无法找到依赖,则会出现编译错误和说明信息.
在Play中使用MacWire
由于MacWire的主要功能是用于简化手动DI,因此它在Play中的用法跟手动DI很相似.仍然需要实现一个自定义的应用加载器来初始化DI模块:
import controllers.BooksController
import play.api.BuiltInComponents
import play.api.cache.EhCacheComponents
import services.{BooksService, CachingBooksService}
import router.Routes
class ApplicationLoader extends play.api.ApplicationLoader {
def load(context: Context) = new ApplicationModule(context).application
}
class ApplicationModule(context: Context)
extends BuiltInComponentsFromContext(context)
with EhCacheComponents {
import com.softwaremill.macwire._
lazy val cache: CacheApi = defaultCacheApi
lazy val booksService: BooksService = wire[CachingBooksService]
lazy val booksController = wire[BooksController]
lazy val router = wire[Routes]
}
上面例子中有趣的部分是ApplicationModule
模块中cache
项也必须被声明,尽管EhCacheComponents
已经提供了defaultCacheApi
.这是因为EhCacheComponents
两个成员都包含CacheApi
值,wire
宏命令不能通过自身来决定在装载CachingBooksService
时使用哪个值,通过定义一个cache
来告诉wire
使用defaultCacheApi
.
总结
MacWire是一个拥有手动DI所有好处的库,同时又通过wire
提供了很多帮助.如果你想要一个编译时DI并且不担心宏命令会打乱你的代码,这是一个可靠的选择.
蛋糕模式
蛋糕模式是一个流行的编译时依赖注入机制,使用Scala的语言特性将组件进行连接.在这种模式中,你所创建的每个组件都需要实现一个特质来描述他.一个DI模块(蛋糕)通过”堆叠”组件特质的方式创建,比如模块类混入了所有组件特质.Play提供了准备完成的组件特质来支持这种方式.相对于Guice的方案,这种方式需要更多的模板代码和隐式配置.
定义组件
最简单的方式中,一个依赖于CacheApi
的BookService
可以这么定义:
trait BookServiceComponent {
def cache: CacheApi //a dependency
lazy val bookService = new BookService(cacheApi)
}
class BookService(cache: cacheApi) {
// ...
}
另一种方式是使用self-type
注解:
trait CacheComponent {
lazy val cache: CacheApi = // ...
}
然后:
trait BookServiceComponent {
this: CacheComponent => //a dependency
lazy val bookService = new BookService(cacheApi)
}
class BookService(cache: cacheApi) {
// ...
}
上面的例子中我们不必要求BookServiceComponent
特质合适被混入,但是混入它的类必须实现一个CacheComponent
特质.可以同时声明多个依赖:
trait BookServiceComponent {
this: CacheComponent with OtherComponent =>
lazy val bookService = new BookService(cacheApi, otherComponent)
}
然后是模块的定义方式:
class Module extends CacheComponent with BookServiceComponent
然后当你初始化一个Moudle
类时,模块中的所有组件都会对他们的依赖进行注入 - 只是按照语言规则进行.最后可以通过这个Module
实例来获得所有组件的实例.
从接口中分离实现
在真实的应用中,你通常会为组件定义一个接口特质和实现.使用self-type
的蛋糕模式可以以类似的方式实现:
trait BookService {
// ...
}
trait BookServiceComponent {
def bookService: BookService
}
class CachingBookService(cache: cacheApi) extends BookService {
// ...
}
trait CachingBookServiceComponent extends BookServiceComponent {
this: CacheComponent =>
lazy val bookService = new CachingBookService(cache)
}
如果使用上述片段中的方式,你可以创建一个依赖于其他组件的组件,而不需要真正知道他们实际的实现:
trait BooksControllerComponent {
this: BookServiceComponent => //dependency on any implementation of BookServiceComponent
// ...
}
在Play中实现蛋糕模式
和手动DI一样,你需要创建一个自定义的ApplicationLoader
实现.同样,BuiltInComponentsFromContext
类会为你的蛋糕提供Play的核心组件.
import controllers.BooksControllerComponent
import play.api.ApplicationLoader.Context
import play.api.cache.EhCacheComponents
import router.Routes
import services.CachingBooksServiceComponent
class ApplicationLoader extends play.api.ApplicationLoader {
def load(context: Context) = new ApplicationModule(context).application
}
class ApplicationModule(context: Context)
extends BuiltInComponentsFromContext(context)
with BooksControllerComponent
with CachingBooksServiceComponent
with EhCacheComponents {
lazy val router = new Routes(httpErrorHandler, booksController)
}
上面的例子中,ApplicationLoader
类中定义了初始化一个蛋糕类似的ApplicationModule
类来提供服务.BuiltInComponentsFromContext
提供了Play内部组件的默认实现,比如Application
和Configuration
实例.CachingBooksServiceComponent
需要一个缓存实现,因此Play的EhCacheComponents
特质同样混入到了模块中.如果你使用了默认的路由器,Routes
会在运行时通过routes
文件生成,因此不能再BuiltInComponentsFromContext
创建,需要你自己进行初始化.
总结
类似于手动DI,蛋糕模式的好处是进行编译时的连接验证,并且不依赖于外部的库.组件的构造是手动的,但是与手动DI不同的是连接过程有Scala编译器自动完成.不好的地方是需要你编写比手动DI更多的代码.
THE READER MONAD
这是个什么东西
读者monad根本上是一个一元函数的monad,它使用Function1.andThen
方法作为一个monadic的map
操作.
要理解读者(reader)是什么,首先考虑下面的代码:
val square = (x: Int) => x * x
val divBy2 = (x: Int) => x / 2.0
square
函数的类型是Int => Int
,divBy2
的类型是Int => Double
,你可以将他们链接在一起,然后生成一个新的Int => Double
函数:
val chained = square.andThen(divBy2)
chained(3) //equal to divBy2(square(3))
reader会把你的一元函数转换为monad,转换后允许你在for表达式中进行链接调用或者其他的monad操作.你可以定义自己的reader实现或者使用现有库中提供的实现.这里我会使用scalaz
作为示例:
import scalaz.Reader
val square = Reader((x: Int) => x * x)
val divBy2 = Reader((x: Int) => x / 2.0)
val chained = square.map(divBy2)
chained(3) // equivalent to divBy2(square(4))
这个例子跟之前的例子类似但是使用了reader而不是简单的函数.然后看下面的片段:
import scalaz._
val squareAndDivBy2Sum = for {
s <- square
d <- divBy2
} yield s + d
sumSquareAndDivBy2(4) // equivalent to square(4) + divBy2(4)
上面的例子创建了一个reader等效于一个函数:
(x: Int) => square(x) + divBy2(x)
只是通过一个for
表达式,它是下面语句的一个语法糖:
square flatMap (s => divBy2 map (d => s + d))
这会根据个人风格提升或降低可读性,或者与函数式编程更加接近的方式.
释放reader的力量进行DI
有多个方法来声明一个依赖,可以声明为一个类的构造器参数,表示整个类需要这个依赖才能工作:
class BooksService(cache: CacheApi) {
// ...
}
这根前面章节的方式类似.另一种方式是将依赖声明为一个方法参数,然后,只有这个方法需要这个声明的依赖:
class BooksService {
def get(id: Int)(cache: CacheApi) : Book = { //this method has a dependency on the cache
// ...
}
def doOtherThing(book: Book) { //this method doesn't have a dependency on the cache
// ...
}
}
这样会非常好,如果一个只需要调用doOtherThing
的客户端根本不需要去关心CacheApi
.另一个好处是方法不用访问那些他们不需要的实例.缺陷是会是方法的参数列表过于混乱.将包含依赖的参数列表标记为implicit
降低这种影响但是也不能完全解决.
构造器注入和参数注入都需要在方法被调用时这些依赖都已经被初始化并能够使用了.这个使用reader monad不同.那如何来使用reader进行依赖注入呢? 现在看下面的例子:
class BooksService {
def get(id: Int) = Reader[CacheApi, Book] { cache =>
cache.getOrElse(id) {
def freshBook = fetchFreshBook(id)
cache.set(s"book$id", freshBook, 2.minutes)
freshBook
}
}
}
可以发现,get
方法并没有把依赖声明为参数,而是声明为一个返回类型.它并不是返回一个Book
,它返回一个 接收依赖(CacheApi)并返回Book
的 reader.因此当客户端调用service.get(1)
时并不会直接返回一个Book
实例,它会返回一个reader,当你提供了相应的依赖时,这里是CacheApi
,它才会返回给你一个Book
实例.
val bookReader = service.get(4) //get the reader
val book = bookReader.run(cache) //read the value by providing the dependency
上面的片段可以简化为:
val book = service.get(4)(cache)
好处是可以将service.get(4)
返回的reader与其他reader进行组合,进行一些转换,最终提供所有的依赖并获取结果.
你妈妈没有告诉你的
有些人争论reader monad可以取代其他依赖注入机制的功能.我表示这并不完全对.虽然reader monad是一个非常有用的工具,但是我认为,如果作为一个DI技术,它更好的是跟面向对象的DI机制结合使用.问题是当你在方法签名中声明依赖时,对reader monad来说是完成了,但是你不能从其他接口中拆分当前组件实现的依赖.比如下面的例子:
trait BooksService {
def get(id: Int) : Book
}
class CachingBooksService extends BooksService {
override def get(id: Int) = Reader[CacheApi, Book] {
// ...
}
}
很显然编译会错误,因为CachingBooksService.get
方法的返回类型与BooksService.get
方法的返回类型不同.通常你希望对BooksService
的客户端隐藏对CacheApi
的依赖,使用reader monad方式是不能实现的.我的做法是使用面向对象DI机制,然后在可用的地方使用reader monad作为补充.
让我们看一下Guice是如何跟reader monad一起使用:
object Book {
implicit val jsonFormat = Json.format[Book]
def get(id: Int) = Reader[BooksService, Option[Book]] { service =>
service.get(id)
}
def list() = Reader[BooksService, Seq[Book]] { service =>
service.list
}
}
case class Book(id: Int, title: String) {
def save() = Reader[BooksService, Unit] { service =>
service.save(this)
}
}
trait BooksService {
def list: Seq[Book]
def get(id: Int): Option[Book]
def save(book: Book): Unit
}
class CachingBooksService @Inject() (cache: CacheApi) extends BooksService {
// ...
}
class BooksController @Inject()(booksService: BooksService) extends Controller {
def get(id: Int) = Action {
Book.get(id).map {
case None => NotFound
case Some(book) => Ok(Json.toJson(book))
}.run(booksService)
}
def list = Action {
Book.list().map { books =>
Ok(Json.toJson(books))
}.run(booksService)
}
def updateTitle(id: Int) = Action(parse.text) { request =>
Book.get(id).map {
case Some(book) =>
book.copy(title = request.body).save()
NoContent
case None => NotFound
}.run(booksService)
}
}
注意: 事实上定义多个不同的模块比在一个模块中加入逻辑要好.
参考列表
- Guice contributors. Guice User’s Guide: Just-in-time Bindings
- Guice contributors. Guice User’s Guide: Minimize mutability
- Fowler, Martin. Inversion of Control Containers and the Dependency Injection pattern
- Fowler, Martin. Anemic Domain Model
- Link, René. Anemic vs. Rich Domain Models
- Guice contributors. Guice User’s Guide: Avoid conditional logic in modules
- DI in Play Framework Using Scala