Posted on 2014-01-28 tags: scala, oop
私はオブジェクト指向や特に自動テスト周りの実務経験に乏しいわけですが、最近になってようやくテストをきちんと書いたりテストファーストによって良い設計になるみたいな実感を得たりしています。長らくテストを書かない文化にいたので、注意しないとすぐにモノリシックな設計になってしまい、後から「テスト書くのどうすんだこれ」みたいになってしまうことも多い。
で、最近 Dependency Injection という依存性をうまいこと抽象化しておく仕組みについて学んだので、その Scala における代表的なデザインパターンである Cake Pattern で実装した話です。
依存性の注入とか訳される、依存しているオブジェクトを直接クラスの中に持っておくのではなくコンストラクタとかで受け取れるようにして依存性を分離しておく仕組みです。例えば Twitter のボットアプリケーションを想定した次のようなコード
1 2 3 4 5 6 7 | object TwitterService { def tweet(text: String, inReplyTo: Option[Int]): Int = { ... } def getTimeline(count: Int): Seq[Status] = { ... } } object TwitterBot { def eventLoop() { ... } } |
において、 TwitterBot
オブジェクトは TwitterService
オブジェクトに依存しています。ここで
TwitterService.tweet(text, inReplyTo)
はツイートした結果発言 ID を返すTwitterService.getTimeline(count)
はタイムラインの最新 count
件を取得するものとします。例が適当なため登場していないが実際他にもいろんなオブジェクトに依存していることでしょう。
ここで TwitterService
はシングルトンとして定義されており、例えば次のように
TwitterBot
で利用されています。
1 2 3 4 5 6 7 8 9 10 11 12 13 | object TwitterBot { def eventLoop() { val statusIds = action(10) ... Thread.sleep(60 * 1000) } def action(count: Int): Seq[Int] = { val tl = TwitterService.getTimeline(count) tl.map { status: Status => TwitterService.tweet("@" + status.userId + " おやすみ〜", Some(status.id)) } } } |
さてこの実装には問題があります。この場合、 TwitterService
が直接
TwitterBot
に出てきているため、これをモックして一時的に振る舞いを変えるということができないのです。すると TwitterBot
を単体テストしようとしても必然的に
TwitterService
の動作を前提としてしまうため単体テストできません。単体テストができないとすべてを結合して粒度の大きなテストしかできなくなるため、問題が起きたときに問題の切り分けが難しくなります。この例はまだ単純だからよいのですが、他にもいろんなオブジェクトに依存しているとすると依存オブジェクト一つ一つを調べなければならないため、問題箇所の特定が難しくなるのです。
ではどうするのかというと、依存性を抽象的に宣言しておいてそのインターフェイスに対してメッセージを呼ぶということをやります。具体的には次のようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | trait TwitterService { def tweet(text: String, inReplyTo: Option[Int]): Int def getTimeline(count: Int): Seq[Status] } class TwitterBot(twitterService: TwitterService) { def eventLoop() { val statusIds = action(10) ... Thread.sleep(60 * 1000) } def action(count: Int): Seq[Int] = { val tl = twitterService.getTimeline(count) tl.map { status: Status => twitterService.tweet("@" + status.userId + " おやすみ〜", Some(status.id)) } } } |
TwitterService
がトレイトになり、 TwitterBot
がクラスになってコンストラクタに TwitterService
のインスタンスを取るようになりました。注目すべきは、コンストラクタに渡している twitterService
は TwitterService
トレイトのオブジェクトであると言っているだけで、その具体的な実装クラスは指定されていないことです。
こうすると、 TwitterBot
クラスの単体テストにはモックした TwitterService
のインスタンスを渡せば良くなり、依存しているオブジェクトの振る舞いを一時的に変えるということができるようになります。
これを使った TwitterBot
クラスの単体テストは次のように書けます。
Mockito と specs2 を使った例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | import org.mockito._ import org.specs2.mutable._ class TwitterBotSpec extends Specification { val twitterService = Mockito.mock(classOf[TwitterService]) val twitterBot = new TwitterBot(twitterService) "action" should { "return status ids" in { val status1 = Status(id = 1, userId = "foo", text = "Hello") val status2 = Status(id = 2, userId = "bar", text = "Scala") val statuses = Seq(status1, status2) Mockito.when(twitterService.getTimeline(2)).thenReturn(statuses) val id1 = 100 val id2 = 102 Mockito.when(twitterService.tweet("@foo おやすみ〜", Some(1))).thenReturn(id1) Mockito.when(twitterService.tweet("@bar おやすみ〜", Some(2))).thenReturn(id2) twitterBot.action(2) must_== Seq(id1, id2) } } } |
この例では new TwitterBot(twitterService)
としてモックしたオブジェクトを注入しており、さらにテストケース内では Mockito
の機能を使って twitterService
オブジェクトの振る舞いをモックしています。 TwitterBot.action
メソッドのテストが、依存しているオブジェクトをうまくモックした状態で実行できているのがわかります。
このように、あるクラスが依存している別のオブジェクトを、そのクラス内で直接インスタンス化せず、後から注入 (inject) できるような設計にしておきます。テストの際には依存しているオブジェクトのところに挙動を適宜モック・スタブしたオブジェクトを渡せば、依存オブジェクトの振る舞いを一時的に固定することができ、テストしやすくなる。これを Dependency Injection といいます。
Cake Pattern では Scala の自分型アノテーション (self-type annotation) という機能を使います。これはクラスを定義するときに自分の型をそのクラス以外にもできるというものです。
1 2 3 4 5 6 7 | trait A { def print(x: String) = println(x) } trait B { val x: String } class C { // 自分の型は A と B をミクスインしたものである self: A with B => def f() = print(x) } |
のようにすると、 C
は外からはただの C
として見えますが、自分の中からはトレイト A
と B
をミクスインしているとみなせます。 B.x
の具体的な実装は書いていませんがコンパイルは通ります。ただし、 C
をインスタンス化するときに問題が起こります。
1 2 3 4 | scala> val c = new C <console>:10: error: class C cannot be instantiated because it does not conform to its self-type C with A with B scala> val c = new C with A with B <console>:10: error: object creation impossible, since value x in trait B of type String is not defined |
B.x
が定義されていないためインスタンスを作成できないと出ます。そこでミクスインのときに B.x
を定義してやります。
1 2 3 | scala> val c = new C with A with B { val x: String = "Hello" } scala> c.f Hello |
(A
と B
を両方不完全な実装にしておくと似たような構文
new C with A { def print() { ... } } with B { val x = ... }
でインスタンス化することはできませんでした。これできるのか?)
これ何が嬉しいのという話なんですが、外からは C
として見えるが実は A
と B
に依存しているというのが表せるのです。「C
というクラスが A
と B
に依存していて、かつその依存している実装を後から指定できる」というわけです。 Dependency
Injection の臭いがしてきました。
さて、ようやく Cake Pattern に入ります。以下は 実戦での Scala: Cake パターンを用いた Dependency Injection (DI) を参考にしたものです。
ユーザーを表すケースクラスとそれを扱うクラスを定義します。
1 2 3 4 5 6 7 8 9 10 | case class User(name: String, email: String) class UserRepository { def authenticate(user: User): User = { println("authenticating: " + user) user } def create(user: User) = println("creating: " + user) def delete(user: User) = println("deleting: " + user) } |
ここで UserRepository
クラスは単にユーザーを扱う関数を集めている名前空間の役割しかないため、 object
として定義したくなる。しかし後のために普通のクラスとして定義しています。
次にもう少しユーザーを扱うために抽象化されているはずである UserService
を定義します。
1 2 3 4 5 6 7 8 | object UserService { def authenticate(name: String, email: String): User = UserRepository.authenticate(User(name, email)) def create(name: String, email: String) = UserRepository.create(User(name, email)) def delete(user: User) = UserRepository.delete(user) } |
上で見たとおり、ここで UserRepository
に依存しているとテストしにくくなります。そこで UserRepository
を注入してほしいオブジェクトとしておくのでした。
1 2 3 4 5 6 7 8 | class UserService { def authenticate(name: String, email: String): User = userRepository.authenticate(User(name, email)) def create(name: String, email: String) = userRepository.create(User(name, email)) def delete(user: User) = userRepository.delete(user) } |
userRepository
はここでは定義されていませんが、これが注入してほしいオブジェクトであることを覚えておきます。さて、ここからまず UserRepository
を名前空間トレイトに包みます。ここで userRepository
は後から注入されるべきオブジェクトとして宣言だけしておきます。
1 2 3 4 5 6 7 8 9 10 11 | trait UserRepositoryComponent { val userRepository: UserRepository class UserRepository { def authenticate(user: User): User = { println("authenticating user: " + user) user } def create(user: User) = println("creating user: " + user) def delete(user: User) = println("deleting user: " + user) } } |
同じく UserService
を名前空間トレイト UserServiceComponent
に包みますが、そこで UserRepositoryComponent
に依存していることを自分型アノテーションで示します。
1 2 3 4 5 6 7 8 9 10 11 | trait UserServiceComponent { self: UserRepositoryComponent => val userService: UserService class UserService { def authenticate(username: String, password: String): User = userRepository.authenticate(username, password) def create(username: String, password: String) = userRepository.create(new User(username, password)) def delete(user: User) = userRepository.delete(user) } } |
最後に、これらのトレイトを継承した環境トレイトを作ります。ここでは実際にアプリケーションに使うものを RealWorld
としました。そしてそのトレイト内で
UserRepositoryComponent
と UserServiceComponent
をインスタンス化して保持しておきます。
1 2 3 4 5 6 | trait RealWorld extends UserRepositoryComponent with UserServiceComponent { val userRepositoryComponent = new UserRepositoryComponent val userServiceComponent = new UserServiceComponent } |
こうしておくと、 Main
オブジェクトで RealWorld
を継承するだけでアプリケーション用のコードを書くことができます。すべての依存性が RealWorld
トレイトに集まっているのがわかります。
1 2 3 4 5 | object Main extends RealWorld { def main(args: Array[String]) { ... } } |
また、テスト時には次のようにすべての依存性をモックした TestEnvironment
を使うことができます。
1 2 3 4 5 6 | trait TestEnvironment extends UserRepositoryComponent with UserServiceComponent { val userRepositoryComponent = mock[UserRepositoryComponent] val userServiceComponent = mock[UserServiceComponent] } |
そして、単体テストしたいクラスは実際にインスタンス化して試すとよいでしょう。
1 2 3 4 | class UserServiceSpec extends Specification with TestEnvironment { override val userService = new UserService ... } |
実際に動く完全なコードは一番下に GitHub へのリンクを置いておきました。これを見るとうまいこと依存性を分離して保管できており、しかもテスト時にもうまくモックできていることがわかります。
自分型アノテーションを使うところ、べつに
UserServiceComponent extends UserRepositoryComponent
と書いても問題なく動くのだけど、自分型アノテーションを使うのは環境トレイトを作るときに依存性を見やすくすることが目的だろうか。つまり
1 2 3 4 5 6 7 8 9 10 11 12 | trait UserRepositoryComponent { ... } trait UserServiceComponent extends UserRepositoryComponent { ... } trait RealWorld extends UserServiceComponent { val userRepositoryComponent = ... val userServiceComponent = ... } |
と書いても問題なく動くのだが、それより
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | trait UserRepositoryComponent { ... } trait UserServiceComponent { self: UserRepositoryComponent => ... } trait RealWorld extends UserRepositoryComponent with UserServiceComponent { val userRepositoryComponent = ... val userServiceComponent = ... } |
と書くほうが RealWorld
が extends
しているトレイトと注入している依存性が一対一に対応するぶん見やすい。
あと Haskell で似たようなことできないかなーと思って探してみたら予想通り型クラスを使った それっぽい論文 を見つけたのでそのうち読む。
今回書いたコードは GitHub にまとめておいた。 TwitterBot
の例も Cake Pattern で書いてみたよ!といっても同じことを繰り返しただけだけど…