Play! Framework 系列(四):DI 模式比較

ScalaCool發表於2018-04-02

本文由 Shaw 發表在 ScalaCool 團隊部落格。

Play! Framework 系列(三)中我們簡單介紹了一下 Play 框架自身支援的兩種依賴注入(執行時依賴注入、編譯時依賴注入)。相信大家對 Play! 的依賴注入應該有所瞭解了。本文將詳細地介紹一些在日常開發中所採用的依賴注入的方式,以供大家進行合理地選擇。

Guice 和 手動注入

上一篇文章中我們所介紹的「執行時依賴注入」以及「編譯時依賴注入」就是用的 Guice 以及手動注入,在這裡就不作詳細介紹了,大家可以去看看上篇文章以及相應的 Demo

接下來我們介紹比較常用的依賴注入模式。

cake pattern(蛋糕模式)

我們首先介紹一下 Scala 中比較經典的一種依賴注入的模式—— cake pattern(也叫“蛋糕模式”),“蛋糕模式”也屬於「編譯時依賴注入」的一種,她不需要依賴 DI 框架。那 “蛋糕模式” 是如何實現的呢?我們知道,在 Scala 中,多個 trait(特質)能夠 “混入” 到 class 中,這樣在某個 class 中我們就能夠得到所有 trait 中定義的東西了。“蛋糕模式”就是基於此種特性而實現的。

接下來我們就通過一個例子來了解一下“蛋糕模式”:

我們需要在頁面上顯示一個包含所有會員資訊的會員列表,需要顯示的內容有:

  1. 會員資訊
  2. 會員卡的資訊

需求很簡單,接下來我們用程式碼組織一下業務:

我們需要從資料庫中查詢「會員卡」以及「會員」的資訊,所以這裡我們首先定義一個資料庫連線的類: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 系列的文章從上一篇到現在拖了太久了,非常抱歉,感謝一直以來的關注,後面我會加快寫作節奏的,本文的例子請戳原始碼連結

Play! Framework 系列(四):DI 模式比較

相關文章