這是 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 改進的方式

1
2
3
4
5
6
class MovieLister {
  private MovieFinder finder;
  public MovieLister() {
    finder = new ColonDelimitedMovieFinder("movies1.txt");
  }
}

Martin 提出的 DI 型式分別是 Constructor Injection, Setter Injection, and Interface Injection.

Constructor Injection

1
2
3
4
class MovieLister...
  public MovieLister(MovieFinder finder) {
      this.finder = finder;
  }

Setter Injection

1
2
3
4
5
6
7
class MovieLister...
  private MovieFinder finder;

   public void setFinder(MovieFinder finder) {
    this.finder = finder;
   }
}

Interface Injection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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());
  }
}

JSR-330 Annotation

而後,這些 DI 的用法,也被 J2EE 給採用,變成 JSR-330 標準,用法如下

Constructor Injection

1
2
3
4
5
6
7
8
class MovieLister...
  @Inject public MovieLister(MovieFinder finder) {
      this.finder = finder;
  }

public interface MovieFinder {
    List findAll();
}

Property Injection

1
2
3
class MovieLister {
  @Inject private MoveFinder finder;
}

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 )

1
2
3
4
5
6
7
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 的方式綁在一起。

接下來,我們就看看要怎麼樣用 Cake Pattern 重新實作上面的 MovieFinder 的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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 的實作 ;這邊的寫法是

1
val movieLister = new MovieLister extends NilMovieFinderComponent

Component Registry

在應用上,由於我們的系統可能包含不只一個元件,而一個物件,可能須要多個不同的元件,因此, Jonas建議我們,如 Guice 用 Module 來定義所有元件的實作類別,在Cake Pattern上,我們 可以用一個 ComponentRegistry Object 來把所有的實作類別指定好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 都需要更改他的實作才行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 所完成的專案,由於我個人並沒有實際使用的經驗 ,所以只能就他所提供的文件做個概述。

1
2
3
4
5
6
7
8
9
10
11
12
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 就可以了解到,某一個物件到底是相依在那些元件之上。

好不容易爭來的控制權,又還了一大半回去

Reader Monad

跳脫了從 OO 出發而來的 DI 方式, Runar Oli 在 Northeast Scala Symposium 2012上 ,提出了一個 純用 Functional Programming 的 DI 實作 (slides)

Runar 用的例子是一個使用 JDBC 的範例

1
2
3
4
5
6
7
8
9
10
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 做一些操作。

1
2
3
4
5
6
7
8
9
10
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 可 以堆疊起來。

1
2
3
4
5
6
7
8
9
10
11
12
13
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 的呼叫,只是單純的 把函式加乘起來,等到最後再來選定執行環境。

1
2
3
4
5
6
7
8
9
10
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 中要怎麼使用呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 的演講 有給一個範例,請大家移步去看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)
}

總結

There is no silver bullet, choose your solution wisely.