Scala編程的蛋糕模式和依賴注入
如果你是一個Java開發者,熟悉 依賴注入 模式, 深度依賴Spring框架的話,在使用Scala做開發時,會遇到一個問題,在Scala世界里,如何實現類似Spring框架的依賴注入呢?
盡管函數式編程的信徒認為他們不需要DI框架,高階(high-order)函數足夠了。但是對于同時支持面向對象的編程和函數式編程的Scala來說,依賴注入是很好的實現應用的一種設計模式。
蛋糕模式(Cake pattern)是Scala實現依賴注入的方式之一。
蛋糕模式是 Scala 之父 Martin Odersky 在其論文 Scalable Component Abstractions 中首先提到。
什么是蛋糕模式呢? 一個非正式的但是很形象的解釋是:
- 蛋糕有很多風味, 你可以根據你的需要增加相應的風味。依賴注入就是增加調料。
- 蛋糕有很多層 (layer)。如果你想要一個更大的蛋糕你就可以增加更多的層。
我們先以Spring常用的User數據庫讀取的實現為例,看看Scala 風格的依賴注入(蛋糕模式)是如何實現的。
Java/Spring風格的依賴注入實現
一個傳統的Spring實現是將程序劃分為"Repository"層(DAO layer) 和Service層。
case class User (name:String)
trait UserRepository {
def create(user: User)
def find(name: String)
def update(user: User)
def delete(user: User)
}
class MockUserRepository extends UserRepository {
def create(user: User) = println( "creating user: " + user)
def find(name: String) = println( "finding user: " + name)
def update(user: User) = println( "udating user: " + user)
def delete(user: User) = println( "deleting user: " + user)
}
class UserService {
@Resource
var userRepository: UserRepository = _
def create(user: User) = userRepository.create(user)
def find(name: String) = userRepository.find(name)
def update(user: User) = userRepository.update(user)
def delete(user: User) = userRepository.delete(user)
}
我們可能有多個UserRepository的實現, 比如JPA, JDBC, Hibernate, iBATIS 等,然后將具體的實現通過Spring注入到 UserService中。這里我們用一個Mock來簡單這些這個Trait, 然后模擬Spring注入,(Spring會根據配置和Reflect自動實現實例化和注入,這里我們只是模擬其原理,并沒有使用Spring框架):
object Main extends App {
val service = new UserService()
val userRepository = new MockUserRepository()
service.userRepository = userRepository // inject userRepository into userService
service.create(User( "user" ))
}
Scala/Cake pattern實現依賴注入
和上面的代碼類似,我們有三個class/trait:UserRepository,MockUserRepository和UserService, 其中MockUserRepository是UserRepository的具體實現,
現在我們想把MockUserRepository注入到UserService。注意UserService和UserRepository目前沒有任何依賴關系。
Scala的蛋糕模式中我們需要聲明幾個Component:
trait UserRepositoryComponent {
val userRepository:UserRepository
}
和
trait UserServiceComponent {
this : UserRepositoryComponent =>
val userService: UserService
}
這里使用 self-type annotation 聲明UserServiceComponent需要UserRepositoryComponent(this: UserRepositoryComponent =>)。 如果需要多個依賴,可以使用下面的格式:
this : Foo with Bar with Baz =>
剩下的就是注入了,生成一個ComponentRegistry對象:
object ComponentRegistry extends UserServiceComponent with UserRepositoryComponent {
override val userRepository: UserRepository = new MockUserRepository
override val userService: UserService = new UserService(userRepository)
}
挺漂亮的實現, 如果相應的依賴沒有提供,或者拼寫錯誤等,編譯時能立刻提示我們。
這還有一個好處就是所有的對象都是val類型的。
這樣你就可以通過ComponentRegistry.userService來使用頂層的組件了。
這樣我們可以實現一種"干凈"的方式實現不同的組件注入,比如我們想單元測試userService,其中它的依賴userRepository通過mock的方式提供:
import org.scalatest.mock.MockitoSugar
import org.mockito.Matchers._
import org.mockito.Mockito._
trait TestEnvironment extends UserServiceComponent with UserRepositoryComponent {
val userRepository = MockitoSugar.mock[UserRepository]
val userService = new UserService(userRepository)
}
object Test extends App with TestEnvironment {
val user = User( "user" )
userService.create(user)
verify(userRepository, times( 1 )).create(any[User])
}
可以很方便的為測試實現新的依賴注入,其中userRepository由mock實現。
這就是蛋糕模式的實現。通過 component trait 抽象接口和依賴,最后在一個ComponentRegistry注入各個具體的實現。