本文由 Shaw 發表在 ScalaCool 團隊部落格。
在 Play! Framework 系列(三)中我們簡單介紹了一下 Play 框架自身支援的兩種依賴注入(執行時依賴注入、編譯時依賴注入)。相信大家對 Play! 的依賴注入應該有所瞭解了。本文將詳細地介紹一些在日常開發中所採用的依賴注入的方式,以供大家進行合理地選擇。
Guice 和 手動注入
在上一篇文章中我們所介紹的「執行時依賴注入」以及「編譯時依賴注入」就是用的 Guice 以及手動注入,在這裡就不作詳細介紹了,大家可以去看看上篇文章以及相應的 Demo
接下來我們介紹比較常用的依賴注入模式。
cake pattern(蛋糕模式)
我們首先介紹一下 Scala 中比較經典的一種依賴注入的模式—— cake pattern(也叫“蛋糕模式”),“蛋糕模式”也屬於「編譯時依賴注入」的一種,她不需要依賴 DI 框架。那 “蛋糕模式” 是如何實現的呢?我們知道,在 Scala 中,多個 trait(特質)能夠 “混入” 到 class 中,這樣在某個 class 中我們就能夠得到所有 trait 中定義的東西了。“蛋糕模式”就是基於此種特性而實現的。
接下來我們就通過一個例子來了解一下“蛋糕模式”:
我們需要在頁面上顯示一個包含所有會員資訊的會員列表,需要顯示的內容有:
- 會員資訊
- 會員卡的資訊
需求很簡單,接下來我們用程式碼組織一下業務:
我們需要從資料庫中查詢「會員卡」以及「會員」的資訊,所以這裡我們首先定義一個資料庫連線的類:DatabaseAccessService 來對相應的資料庫進行操作:
trait DatabaseAccessServiceComp {
val databaseAccessService = new DatabaseAccessService()
}
class DatabaseAccessService{
...
}
複製程式碼
大家可能會發現,在我們之前文章中的 service 中並沒有定義 trait,而這裡卻定義了,並且在 trait 中,我們例項化了 DatabaseAccessService, 這就是“蛋糕模式”中所需要的,現在看好像並沒有什麼卵用,別急,等我們將所有的 service 都定義好了,她就有用了。
接下來我們定義 WxcardService 以及 WxcardMemberService:
//定義 WxcardService
trait WxcardServiceComp {
this: DatabaseAccessServiceComp =>
val wxcardService = new WxcardService(databaseAccessService)
}
class WxcardService(databaseAccessService: DatabaseAccessService) {
...
}
//定義 WxcardMembrService
trait WxcardMemberServiceComp {
this: DatabaseAccessServiceComp =>
val wxcardMemberService = new WxcardMemberService(databaseAccessService)
}
class WxcardMemberService(databaseAccessService: DatabaseAccessService) {
...
}
複製程式碼
寫法與上面定義的 DatabaseAccessService 沒有什麼區別,因為上面兩個 service 都需要依賴 DatabaseAccessService,所以在特質中用「自身型別」來將其混入,如果需要多個依賴,可以這樣寫:
this DatabaseAccessServiceComp with BarComp with FooComp =>
複製程式碼
最後我們需要定義一個 WxcardController,來將資料傳遞到相應的頁面上去:
class WxcardController (
cc: ControllerComponents,
wxcardService: WxcardService,
wxcardMemberService: WxcardMemberService
) extends AbstractController(cc) {...}
複製程式碼
可以看到 WxcardController 需要依賴我們上面定義的一些 service,那麼在蛋糕模式下,我們怎樣才能將這些依賴注入到 WxcardController 中呢,由於“蛋糕模式”也是「編譯時依賴注入」的一種,那麼我們可以參考上一篇文章中所採用的方式:
同樣,我們需要實現自己的 ApplicationLoader:
//定義 load 那部分程式碼省略了,大家可以去看 Demo
...
class MyComponents(context: ApplicationLoader.Context)
extends BuiltInComponentsFromContext(context)
with play.filters.HttpFiltersComponents
with DatabaseAccessServiceComp
with WxcardServiceComp
with WxcardMemberServiceComp {
lazy val wxcardController = new WxcardController(controllerComponents, wxcardService, wxcardMemberService)
lazy val router: Router = new Routes(httpErrorHandler, wxcardController)
}
複製程式碼
通過上面的程式碼,就完成了注入,可以看到我們定義的所有 xxxServiceComp 特質都被混入到了 MyComponents 中,這樣,當 Play載入時,我們所定義的 service 就都在這裡被例項化了,為什麼呢?因為我們在定義 xxxServiceComp 時,都會有這麼一行程式碼:
val xxxService = new XxxService()
複製程式碼
這就是為什麼我們之前要在每個 service 中都定義一個 trait,因為 Scala 中的 class 可以混入多個 trait,在這裡,我們可以將所有需要的依賴都混入到 MyComponents 中,然後實現注入。
至於為什麼要叫“蛋糕模式”,我個人是這麼理解的: 我們定義的 xxxServiceComp 比如 WxcardServiceComp 相當於蛋糕中的某一層,而那些需要被多次依賴的 xxxServiceComp,比如上面定義的 DatabaseAccessServiceComp 可以看作是蛋糕中的調味料(比如水果,巧克力啥的),將這些蛋糕一層一層地放在一起,然後再混入一些調味料,就組成了一個大的蛋糕—— MyComponents。
可以看到“蛋糕模式”中,我們需要寫非常多的樣板程式碼,要為每個 service 都定義一個 trait,感覺心很累,那麼接下來我們就介紹一種比較輕巧而又簡潔的的方式。
macwire
macwire 是基於 「Scala 巨集」來實現的,我們使用她可以讓依賴注入變得非常簡單,並且使我們的程式碼量減少許多。接下來,我們就通過 macwire 來實現一下上面的例子。
首先在專案中引入 macwire,在 build.sbt 檔案中增加一行依賴:
libraryDependencies ++= Seq(
"org.scalatestplus.play" %% "scalatestplus-play" % "3.0.0-M3" % Test,
//在這裡新增 macwire 的依賴
"com.softwaremill.macwire" %% "macros" % "2.3.0" % Provided,
)
複製程式碼
然後定義 service:
//定義 DatabaseAccessService
class DatabaseAccessService{
...
}
//定義 WxcardService
class WxcardService(databaseAccessService: DatabaseAccessService) {
...
}
//定義 WxcardMembrService
class WxcardMemberService(databaseAccessService: DatabaseAccessService) {
...
}
複製程式碼
可以看到,我們現在就不需要定義 trait 了,接下來,定義 WxcardController:
class WxcardController (
cc: ControllerComponents,
wxcardService: WxcardService,
wxcardMemberService: WxcardMemberService
) extends AbstractController(cc) {...}
複製程式碼
controller 的定義和上面的一樣,接下來,我們就使用 macwire 來實現依賴注入,macwire 也是「編譯時依賴注入」的一種,所以我們同樣需要實現 ApplicationLoader:
import com.softwaremill.macwire._
...
class MyComponents(context: ApplicationLoader.Context)
extends BuiltInComponentsFromContext(context)
with play.filters.HttpFiltersComponents {
lazy val databaseAccessService = wire[DatabaseAccessService]
lazy val wxcardService = wire[WxcardService]
lazy val wxcardMemberService = wire[WxcardMemberService]
lazy val wxcardController = wire[WxcardController]
lazy val router: Router = {
val prefix = "/"
wire[Routes]
}
}
複製程式碼
在上面的程式碼中,我們只需要將相應的依賴通過下面的方式例項化就可以了:
lazy val wxcardService = wire[WxcardService]
複製程式碼
就是在型別外面新增了一個 wire
,這樣就完成了例項化,並且也不需要指定依賴的引數,macwire 會自動幫我們完成例項化和注入:
比如上面的
lazy val databaseAccessService = wire[DatabaseAccessService]
lazy val wxcardService = wire[WxcardService]
lazy val wxcardMemberService = wire[WxcardMemberService]
lazy val wxcardController = wire[WxcardController]
複製程式碼
macwire 就幫我們轉化成了:
lazy val databaseAccessService = new DatabaseAccessService()
lazy val wxcardService = new WxcardService(databaseAccessService)
lazy val wxcardMemberService = new WxcardMemberService(databaseAccessService)
lazy val wxcardController = new WxcardController(controllerComponents, wxcardService, wxcardMemberService)
複製程式碼
我們只需要在定義某個類的時候宣告我們需要哪些依賴,例項化和注入 macwire 都會幫我們去完成,macwire 在例項化某個類的時候,會去當前檔案或者與當前檔案有關的程式碼中查詢相關的依賴,找到了就完成注入,若沒有找到說明該依賴沒有被定義過,或者沒有正確引入。
在日常開發中,我們會建立很多個 service,將所有的 service 放在 MyComponents 中例項化會使得程式碼顯得很臃腫,而且也不便於維護。通常我們會專門定義一個 Module 來組織這些 service:
package config
import com.softwaremill.macwire._
import services._
trait ServicesModule {
lazy val databaseAccessService = wire[DatabaseAccessService]
lazy val wxcardService = wire[WxcardService]
lazy val wxcardMemberService = wire[WxcardMemberService]
}
複製程式碼
這裡我們新建了一個 ServiceModule.scala 檔案來將組織這些 service。
那麼上面的 ApplicationLoader 檔案就可以這樣去寫:
import com.softwaremill.macwire._
...
class MyComponents(context: ApplicationLoader.Context)
extends BuiltInComponentsFromContext(context)
with play.filters.HttpFiltersComponents
with config.ServicesModule {
lazy val wxcardController = wire[WxcardController]
lazy val router: Router = {
val prefix = "/"
wire[Routes]
}
}
複製程式碼
可以看到 macwire 使用起來非常簡單,並且能夠簡化我們的依賴注入。在我們的專案中所採用的是 macwire,所以推薦大家使用 macwire。
結語
關於 Play 中的「依賴注入」到這裡就結束了,希望能夠給大家一些幫助,另外 Play 系列的文章從上一篇到現在拖了太久了,非常抱歉,感謝一直以來的關注,後面我會加快寫作節奏的,本文的例子請戳原始碼連結。