ScalaでDIというかServiceLocator的な名状しがたい何か
ScalaでDIというかServiceLocator的な名状しがたい何かを簡単に実装してみた。
理由はテストを楽に書くため。LiftのSimpleInjectorも検討したが、まぁ、勉強がてら作成。
完成形のコードはgistに上げた
https://gist.github.com/2651257
例えばTwitterのクライアントを作るとする。
TwitterのAPIにアクセスするようなクラスが
class TwitterApi { def publicTimeLines : List[String] = { //Twitter API つかってごにょごにょするはず List("本当は", "リアルに", "public", "timeline", "取得する") } }
それのクライアントのコードが
class TwitterClient extends ApiInjector { def indexedPublicTimeLine : List[String] = { val twitter = new TwitterApi twitter.publicTimeLines.zipWithIndex .map{ case (s, i) => (i + 1) + " " + s} } }
と実装したとする
さぁ!TwitterClientのテストを書け!!!ちゃんと書け!
が、ダメ
実際にAPIにつなぎにいってるので、public time line は毎回変わるのでテストが書けない!!!
まずは、実装とインタフェースを分離する
Scalaでは Trait だね
trait TwitterApi { def publicTimeLines : List[String] }
実装はこの trait を mixin しよう(traitが一個しかないときにはextends。複数ある場合は with)
class TwitterApiImpl extends TwitterApi { def publicTimeLines : List[String] = { //Twitter API つかってごにょごにょするはず List("本当は", "リアルに", "public", "timeline", "取得する") } }
次に Injector な Trait を用意する
trait ApiInjector { var twitter : TwitterApi = new TwitterApiImpl; }
クライアントのコードは Injector の Trait を mixin する
class TwitterClient extends ApiInjector { def indexedPublicTimeLine : List[String] = { twitter.publicTimeLines.zipWithIndex .map{ case (s, i) => (i + 1) + " " + s} } }
これでクライアントの動作は今までと変わらなくなった
さて、ではテストはどう書けばいい?
テスト用に、Apiを偽装してくれる trait を書く
trait TestApiInjector extends ApiInjector { twitter = new TwitterApi { def publicTimeLines = { List ("dummy", "public", "timeline") } } }
テスト時には、TwitterClient にこの Trait を mixin して利用します。new TwitterClient with TestApiInjector がミソ
import org.specs2.mutable._ class TwitterClientTest extends Specification { "Twitter Api Client" should { val client = new TwitterClient with TestApiInjector val indexedPublicTimeLine = client.indexedPublicTimeLine "indexedPublicTimeLine return size " in { indexedPublicTimeLine.size must_== 3 } "indexedPublicTimeLine return indexedLine" in { indexedPublicTimeLine(0) must_== "1 dummy" } } }
ね。簡単でしょう?