2015/08/11

[ScaVa->Scala] Scala中使用Reader Monad來實現Dependency Injection

[ScaVa->Scala] Scala中使用Reader Monad來實現Dependency Injection

[ScaVa->Scala] Scala中使用Reader Monad來實現Dependency Injection

此篇文章同時發步於BlogGist

前言

上週寫了一篇老闆說大家來寫個URL Shortener,裡面的Design Concept使用了Implicit來做DI(Dependency Injection),也就是把DBClient設計成可從外部指定。

但是其實在Scala中使用Functional Programming的特性實現DI的方式不只這一種,在這種想要把DBClient抽取出來而使用Implicit的方式,會有以下兩點要考慮的:

  1. 所有使用到DBClient的function,全都要加上(implicit dbClient:DBClient)這樣的描述,如果外層沒有要用到,而內層有用到,外層還是要加上這樣的描述。
  2. Implicit的概念上應該比較是”使用者可忽略”的,但是DBClient在這邊其實並不是一個需要被忽略的,而只是為了減少大量重覆的code。

所以在這邊我們要嘗試另一個Reader Monad的方式,來實現DI。

Reader Monad

有關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的特性在很多地方使用上都會很方便。

在外面使用上,其實getUsersetUserupdateUserName,這些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上面看嘍。

張貼留言

My World