[ScaVa->Scala] Scala中使用Reader Monad來實現Dependency Injection
前言
上週寫了一篇老闆說大家來寫個URL Shortener,裡面的Design Concept使用了Implicit來做DI(Dependency Injection),也就是把DBClient設計成可從外部指定。
但是其實在Scala中使用Functional Programming的特性實現DI的方式不只這一種,在這種想要把DBClient抽取出來而使用Implicit
的方式,會有以下兩點要考慮的:
- 所有使用到DBClient的function,全都要加上
(implicit dbClient:DBClient)
這樣的描述,如果外層沒有要用到,而內層有用到,外層還是要加上這樣的描述。 - Implicit的概念上應該比較是”使用者可忽略”的,但是DBClient在這邊其實並不是一個需要被忽略的,而只是為了減少大量重覆的code。
所以在這邊我們要嘗試另一個Reader Monad的方式,來實現DI。
Reader Monad
有關Reader Monad,這兩篇文章寫得很清楚:
- Dependency Injection in Scala (中文)
- Scrap Your Cake Pattern Boilerplate: Dependency Injection Using the Reader Monad
裡面寫到的不止是Reader Monad,包含了Scala中常用到的Cake Pattern,以及轉換成使用Reader Monad的好處。
這裡舉個例子,這是本來使用implicit的方式,在updateUserName的function裡,其實並沒有用到DBClient,但是他呼叫的getUser和setUser有使用到,所以他也要寫這個implicit的宣告:
case class User(id:Int,name:String)
case class DBClient(conn:String) {
def getUser(id:Int):User = User(id,"USER"+id)
def setUser(user:User):Boolean = {true}
}
object User{
def getUser(id:Int)(implicit db:DBClient) = {
db.getUser(id)
}
def setUser(user:User)(implicit db:DBClient) = {
db.setUser(user)
}
def updateUserName(id:Int,name:String)(implicit db:DBClient) = {
val user = getUser(id)
val newUser = user.copy(name=name)
setUser(user)
}
}
object TestApp{
implicit val dbClient = DBClient("Test")
def main() {
val user = User.getUser(123)
User.updateUserName(123,"Yo")
}
}
但若使用ReaderMonad的話(這裡的Reader是使用scalaz):
case class User(id:Int,name:String)
case class DBClient(conn:String) {
def getUser(id:Int):User = User(id,"USER"+id)
def setUser(user:User):Boolean = {true}
}
object User{
def getUser(id:Int) = Reader((db:DBClient) => {
db.getUser(id)
})
def setUser(user:User) = Reader((db:DBClient) =>{
db.setUser(user)
})
def updateUserName(id:Int,newName:String) = {
for{
user <- getUser(id)
result <- setUser(user.copy(name=newName))
} yield result
}
}
object TestApp{
val dbClient = DBClient("Test")
def main() {
val user = run(User.getUser(123))
run(User.updateUserName(123,"Yo"))
}
private def run[A](reader: Reader[DBClient, A]): A = {
reader(dbClient)
}
}
這邊可以看到一個有趣的東西,User裡的getUser和setUser因為使用Reader Monad,所以他就有了Monad的特性 (關於Monad可參考之前寫的這兩篇: [ScaVa->Scala] 什麼是Monad?和Category theory(範疇論)之什麼是Functor, Applicative, Monad, Semigroup, Monoid?)。
因此我們在updateUserName這裡,可以直接使用for comprehension的方式把他們串在一起,當然有Monad的特性在很多地方使用上都會很方便。
在外面使用上,其實getUser
、setUser
、updateUserName
,這些function的回傳值都是一個”Function”,這個function要傳入的是DBClient,回傳的是他們本來要回傳的值,只是這個function是使用Reader Monad包起來。
我們對於所有這些同樣型態的Reader Monad,可以定義個統一的處理方式,在這裡就是run
,其實這個run就是一個general的handler,甚至可以做一些轉成Json或是error handling的處理,很方便的!
Shortener
Shortener也更新至0.2.0,以Reader Monad來implement Shortener使用DBClient的部份。
trait Shortener {
def shorter(url:String)(implicit tracerInfo: TracerInfo) : Reader[DBClient, String Or BaseException]
def taller(short:String)(implicit tracerInfo: TracerInfo) : Reader[DBClient, String Or BaseException]
}
詳細的程式請至GitHub上面看嘍。
沒有留言:
張貼留言