Dependency Injection in Scala
這是 4/23 要在 Scala Taiepi Meeting 3 用的講稿,先打出來放這。
Dependency Injection & Inversion of Control
=============================================
`Dependency Injection & Inversion of Control`_ 是 Martin Fowler 在 2004 年所
提出來的一個的概念,Martin Fowler在這篇文章中指出,DI可以三種型式來實作,這觀念後來由 Spring
Source 及 Google 實做出來,變成 Java Enterprise 應用中,不可或缺的一塊。
在 Ioc 觀念的出現前,物件相依的元件(Component),多是於物件建立時,就帶入(binding)的,Martin
的供獻在於,IoC把物件對元件的相依性拆出來,變成可以替換的實作。
底下是 Martin 所舉的例子,接著我們會看到用 DI 改進的方式
.. code-block:: java
class MovieLister {
private MovieFinder finder;
public MovieLister() {
finder = new ColonDelimitedMovieFinder("movies1.txt");
}
}
Martin 提出的 DI 型式分別是 Constructor Injection, Setter Injection, and Interface Injection.
Constructor Injection
---------------------
.. code-block:: java
class MovieLister...
public MovieLister(MovieFinder finder) {
this.finder = finder;
}
Setter Injection
-----------------
.. code-block:: java
class MovieLister...
private MovieFinder finder;
public void setFinder(MovieFinder finder) {
this.finder = finder;
}
}
Interface Injection
-------------------
.. code-block:: java
public interface InjectFinder {
void injectFinder(MovieFinder finder);
}
class MovieLister implements InjectFinder...
public void injectFinder(MovieFinder finder) {
this.finder = finder;
}
class Tester...
private void registerComponents() {
container.registerComponent("MovieLister", MovieLister.class);
container.registerComponent("MovieFinder", ColonMovieFinder.class);
}
private void registerInjectors() {
container.registerInjector(InjectFinder.class, container.lookup("MovieFinder"));
container.registerInjector(InjectFinderFilename.class, new FinderFilenameInjector());
}
}
.. _Dependency Injection & Inversion of Control: http://martinfowler.com/articles/injection.html
JSR-330_ Annotation
-------------------
而後,這些 DI 的用法,也被 J2EE 給採用,變成 JSR-330_ 標準,用法如下
Constructor Injection
^^^^^^^^^^^^^^^^^^^^^
.. code-block:: java
class MovieLister...
@Inject public MovieLister(MovieFinder finder) {
this.finder = finder;
}
public interface MovieFinder {
List findAll();
}
Property Injection
^^^^^^^^^^^^^^^^^^^
.. code-block:: java
class MovieLister {
@Inject private MoveFinder finder;
}
.. _JSR-330: http://docs.oracle.com/javaee/6/api/javax/inject/package-summary.html
Dependency Injection in Scala
=============================
花了許久時間解釋 DI 於 Java 的演進,我們總算可以進入本文的正題 Dependency Injection in Scala。在 Scala 中
實做 DI 的方式有有那些呢,接下來我們要談的就是 DI in Scala 的幾個選擇。
- JSR-330_
- Cake Pattern
- SubCut
- Functional DI - Reader Monad
JSR-330_
-------------
由於 Scala 本身在編譯時,會被編成 Java Byte Code ,所以可以直接套用支源 JSR-330_ 的
框架,但是在使用上有一些小眉角,就是,若是使用 Constructor Injection 時,需要用 `@Inject()` 標
在 class 定義之前,使用 Property Injection 時,由於 Scala 的 `val` 被定義成 final 的,所以不
能夠被用在 Property Injection 之上,在這時候,只能用 `var` 來做。
為了達到 immutable 的效果,我通常都是用 Constructor Injection 的。( 另一個原因是我不喜歡 AOP )
.. code-block:: scala
case class @Inject() MovieLister(finder: MovieFinder)
class MovieLister2 {
var finder: MovieFinder = _
}
Cake Pattern
------------
Cake Pattern 大概是除了 JSR-330 外,最常被提到在 Scala 下的實作 DI 的方式了,Cake Pattern 是由
Scala 之父 Martin Odersky 的一篇論文 `Scalable Component Abstractions`_ 中提到,不過大
多數人看過的版本,應該是 Jonas Bonér 的 `Real-World Scala: Dependency Injection (DI)`_
Cake Pattern 是利用 Scala Mixin 的功能,讓物件被創造時,才把相依的元件,透過 Mixin 的方式綁在一起。
.. _Scalable Component Abstractions: http://lamp.epfl.ch/~odersky/papers/ScalableComponent.pdf
.. _Real-World Scala\: Dependency Injection (DI): http://jonasboner.com/2008/10/06/real-world-scala-dependency-injection-di/
接下來,我們就看看要怎麼樣用 Cake Pattern 重新實作上面的 MovieFinder 的例子
.. code-block:: scala
trait MovieFinderComponent {
def finder: MovieFinder
trait MovieFinder {
def findAll: List[Movie]
}
}
trait NilMovieFinderComponent extends MovieFinderComponent {
val finder: MovieFinder = new NilMovieFinder
class NilMovieFinder extends MovieFinder {
def findAll: List[Movie] = {
Nil
}
}
}
abstract class MovieLister {
this: MovieFinderComponent =>
def findByAuthor(author: String): List[Movie] = {
finder.findAll.filter(m => m.author == author)
}
}
上面便是一個使用 Cake Pattern 實作 MovieFinder 所需要的程式碼;首先,我們要把所需的
元件,包在一個 Component trait (MovieFinderComponent) 裡頭,再把這個原元件的規格,
寫在 Component trait 中的另一個 trait 中(MovieFinder),最後,再是訂一個取存這元件的
method `def finder: MovieFinder`.
在被注入的這一方 (MovieLister) ,我們使用 self-type annotation 來宣告,當要生成一個
MovieLister instance 時,我們必需要提供 MovieFinderComponent 的實作,同時,在MovieLister
內,我們可以透過 `finder` 這個 method 來呼叫 MovieFinder 的實作 ;這邊的寫法是
.. code-block:: scala
val movieLister = new MovieLister extends NilMovieFinderComponent
Component Registry
^^^^^^^^^^^^^^^^^^
在應用上,由於我們的系統可能包含不只一個元件,而一個物件,可能須要多個不同的元件,因此,
Jonas建議我們,如 Guice 用 Module 來定義所有元件的實作類別,在Cake Pattern上,我們
可以用一個 ComponentRegistry Object 來把所有的實作類別指定好。
.. code-block:: scala
object ComponentRegistry extends
MyMovieFinderComponent with
MyAuthorFinderComponent with
MyUserRepositoryComponent
object TestEnvironment extends
MockMovieFinderComponent with
MockAuthorFinderComponent with
MockUserRepositoryComponent
class Test {
def testList {
new lister = new MovieLister extends TestEnvironment
// testing code.
}
}
.. note:: 一個動動腦的時間,在使用 ComponentRegistry 時,我們要怎麼寫,才會讓某一個 Component 的實作變成 Singleton
Pros and Cons of Cake Pattern
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Pros
- no framework required, using only language features
- type safe – a missing dependency is found at compile-time
- powerful – “assisted inject”, scoping possible by implementing the dependency-providing method appropriately
Cons
- lot of boilerplate code
Problem of Cake Pattern
^^^^^^^^^^^^^^^^^^^^^^^
然而,從我的使用經驗來講, Cake Pattern 有個缺點,讓我不推薦各位來使用 Cake Pattern ,缺點是, Cake Pattern
只能使用在第一層相依性的元件上,造成實作上的程式碼的不一致性,以及可重用性的問題。
今天,假設我們電影的數量一直成長,所以我們開始分門別類來存放這些電影的類別,而我們在找作者時,只在各個子類別下
找;為了這個變更,我們不只需要新增一些元件,我們連舊有的 MovieLister 都需要更改他的實作才行。
.. code-block:: scala
trait CategorizedMovieFinderFactoryComponent {
trait CategorizedMovieFinderFactory {
def create(category: String): MovieFinder
}
}
abstract class MovieListerFactory {
this: CategorizedMovieFinderComponent
def create(category: String): MovieLister = new MovieLister(finder)
}
// WTF, the implementation has to change here?!
case class MovieLister(finder: MovieFinder) {
def findByAuthor(author: String): List[Movie] = {
finder.findAll.filter(m => m.author == author)
}
}
SubCut_
========
SubCut_ 是由 Dick Wall 這位 Java Posse Podcaster 所完成的專案,由於我個人並沒有實際使用的經驗
,所以只能就他所提供的文件做個概述。
.. code-block:: scala
object SomeModule extends NewBindingModule({ implicit module =>
import module._ // optional but convenient
bind [ServiceA] toSingle Y
bind [Z] toProvider { codeToGetInstanceOfZ() }
})
class SomeService(param1: String, param2: Int)(implicit val bindingModule: BindingModule)
extends SomeTrait with Injectable {
val service1 = inject [ServiceA]
}
在 SubCut 中,是讓需要被注入的元件庫,使用 implicit variable 的方式,從執行環境中帶入,然後直接讓
SomeService對整個 Module 做存取。
然而,因為 SubCut 的這一個缺陷,我個人是不用去使用 SubCut 的,這個缺點,在 Martin Fowler 文中被稱
做 `Service Locator`_ ,使用 Service Locator Pattern 的缺點是,你把整個環境都傳給了需要被注入的物
件,讓物件自己在環境中去挖寶;這正好就把把 Inversion of Control 的拿交出的控制權又還給了物件本身;
除了去閱讀程式碼外,你無法單看 class constructor 就可以了解到,某一個物件到底是相依在那些元件之上。
好不容易爭來的控制權,又還了一大半回去
.. _SubCut: https://github.com/dickwall/subcut/blob/master/GettingStarted.md
.. _Service Locator: http://martinfowler.com/articles/injection.html#ServiceLocatorVsDependencyInjection
Reader Monad
============
跳脫了從 OO 出發而來的 DI 方式, Runar Oli 在 Northeast Scala Symposium 2012上 ,提出了一個
純用 `Functional Programming 的 DI 實作`_ (slides_)
.. _Functional Programming 的 DI 實作: http://www.youtube.com/watch?v=ZasXwtTRkio
.. _slides: http://dl.dropbox.com/u/4588997/Runar-NEScala-DI.pdf
Runar 用的例子是一個使用 JDBC 的範例
.. code-block:: scala
def setUserPwd(id: String,
pwd: String,
c: Connection) = {
val stmt = c.prepareStatement(
"update users set pwd = ? where id = ?")
stmt.setString(1, pwd)
stmt.setString(2, id)
stmt.executeUpdate
stmt.close
}
上面的程式碼,我們可以用 Functional Way 改寫成底下的方式,讓 `setUserPwd` 改成
回傳一個 (Connection => Unit) 的函式,這個函式,收進一個 DB Connection 然後對
這個 Connection 做一些操作。
.. code-block:: scala
def setUserPwd(id: String,
pwd: String): Connection => Unit =
c: Connection => {
val stmt = c.prepareStatement(
"update users set pwd = ? where id = ?")
stmt.setString(1, pwd)
stmt.setString(2, id)
stmt.executeUpdate
stmt.close
}
接著,我們可以用一個 DB Monad 把所有 DB Operation 都抽像化,讓這些 DB Operation 可
以堆疊起來。
.. code-block:: scala
case class DB[A](g: Connection => A) {
def apply(c: Connection) = g(c)
def map[B](f: A => B): DB[B] =
DB(c => f(g(c)))
def flatMap[B](f: A => DB[B]): DB[B] =
DB(c => f(g(c))(c))
}
def pure[A](a: A): DB[A] =DB(c => a)
implicit def db[A](f: Connection => A): DB[A] = DB(f)
底下是堆疊起來的成果,透過 scala `for comprehension`_ 把 getUserPwd, setUserPwd 堆
疊起來,變成一個 changePwd method ,這一步步的把函式加成,符合了 Functional Programming 中
的 no side-effect ,在函式加成的過程中,我們並沒有去執行任何有 side effect 的呼叫,只是單純的
把函式加乘起來,等到最後再來選定執行環境。
.. code-block:: scala
def changePwd(userid: String,
oldPwd: String,
newPwd: String): DB[Boolean] =
for {
pwd <- getUserPwd(userid)
eq <- if (pwd == oldPwd) for {
_ <- setUserPwd(userid, newPwd)
} yield true
else pure(false)
} yield eq
那 DI 在這個 DB Monad 中要怎麼使用呢?
.. code-block:: scala
abstract class ConnProvider {
def apply[A](f: DB[A]): A
}
def mkProvider(driver: String, url: String) =
new ConnProvider {
def apply[A](f: DB[A]): A = {
Class.forName(driver)
val conn = DriverManager.getConnection(url)
try { f(conn) }
finally { conn.close }
}
}
}
lazy val sqliteTestDB =
mkProvider("org.sqlite.JDBC", "jdbc:sqlite::memory:")
lazy val mysqlProdDB =
mkProvider("org.gjt.mm.mysql.Driver",
"jdbc:mysql://prod:3306/?user=one&password=two")
Runar 是把 DB Connection 的建立用 ConnProvider 抽像化,透過這樣,
我鍆可以簡單的定意兩種不同的執行環境,一組是 MySQL 另一組是測試用的 SQLite,
.. code-block:: scala
def runInTest[A](f: ConnProvider => A): A =
f(sqliteTestDB)
def runInProduction[A](f: ConnProvider => A): A =
f(mysqlProdDB)
def myProgram(userid: String): ConnProvider => Unit =
r: ConnProvider => {
println("Enter old password")
val oldPwd = readLine
println("Enter new password")
val newPwd = readLine
r(changePwd(userid, oldPwd, newPwd))
}
def main(args: Array[String]) =
runInTest(myProgram(args(0)))
以上就是如何用 DB Monad 來達到 DB DI 的功用,這樣做的好處是
- Dead-simple. Just function composition.
- Explicit, type-safe dependencies.
- Lift any function.
- No frameworks, annotations, or XML.
- No initialization step.
- Doesn’t rely on esoteric language features.
More Useful Monad
-----------------
上面的 DB Monad ,只能對 DB 來做 DB ,然而,若是我們再多抽像化一層,把 DB Connection 變成
一個 **type parameter** ,那麼,我們就有一個 Reader Monad ,可以套用在許多不同應用上,
至於實際的用法, `Runar 的演講`_ 有給一個範例,請大家移步去看。
.. code-block:: scala
case class Reader[C, A](g: C => A) {
import Reader._
def apply(c: C) = g(c)
def map[B](f: A => B): Reader[C, B] = {
c: C => f(g(c))
}
def flatMap[B](f: A => Reader[C, B]): Reader[C, B] = {
c: C => f(g(c))(c)
}
}
object Reader {
implicit def reader[A, B](f: A => B): Reader[A, B] = Reader(f)
}
.. _for comprehension: http://stackoverflow.com/questions/1052476/can-someone-explain-scalas-yield
.. _Runar 的演講: http://www.youtube.com/watch?v=ZasXwtTRkio
總結
======
There is no silver bullet, choose your solution wisely.