case classはコンパニオンオブジェクトのなかで定義すべき理由
Scalaでデータなどを扱うときにはcase class
を使うのが一般的である。
このcase class
はコンパニオンオブジェクトに書くのが当たり前のようになっているがそれはなぜか。
ScalaでもJavaと同様、インナークラスを定義できる。
しかし、その違いとして「パス依存性」という特徴がある。
インナークラスの型はどのアウタークラスから生成されたかで決まる。
REPLでアウタークラスとインナークラスを定義し、インスタンスを2つ作る。
var
変数にインスタンスを再代入しようとすると、type mismatch
エラーとなることがわかる。
scala> class Outer { | class Inner { | private val hoge = "hoge" | } | } defined class Outer scala> val o1 = new Outer o1: Outer = Outer@18769467 scala> val o2 = new Outer o2: Outer = Outer@368102c8 scala> val in1 = new o1.Inner in1: o1.Inner = Outer$Inner@77556fd scala> val in2 = new o2.Inner in2: o2.Inner = Outer$Inner@1996cd68 scala> var in = in1 in: o1.Inner = Outer$Inner@77556fd scala> in = in2 <console>:13: error: type mismatch; found : o2.Inner required: o1.Inner in = in2 ^ scala> in1.isInstanceOf[Outer#Inner] res0: Boolean = true
インナークラスの型は、どのインスタンスから生成されたかに依存している。(パス依存性)
その型を汎用的に表記するためにはOuter#Inner
のように記述する必要がある。
さて、このようなパス依存性の特徴を確認したうえで、MVCで設計したControllerからServiceトレイトを利用する例を考えてみよう。
UserController.scala
import UserService._ class UserController extends Controller with UserService { def list: Seq[User] = getUsers }
UserService.scala
object UserService { // データベースから取得するレコードの代わりにモックとしてこのデータを利用する val users: Seq[(Int, String)] = Seq((1, "hoge"), (2, "fuga"), (3, "foo"), (4, "bar")) } trait UserService { import UserService._ case class User(id: Int, name: String) def getUsers: Seq[User] = users.map(u => User(id = u._1, name = u._2)) }
よくある実装のしかただが、ここではあえてUserService
トレイトの中にcase class User()
を定義している。
sbt console
からUserController
を複数インスタンス化し、list
の値を比較してみよう。
scala> val userController1 = new UserController userController1: UserController = UserController@297eb9d9 scala> val userController2 = new UserController userController2: UserController = UserController@4faf14f1 scala> userController1.list == userController2.list res0: Boolean = false
同一のデータ(users
)を利用しているのだから、どのインスタンスから実行しても結果は同値になるはずなのだが、
userController1.list == userController2.list
として比較した結果は、false
として返っている。
これが、先に説明したScalaのインナークラスにおける「パス依存性」に起因する問題である。
このように、今回のUser
のようにインナークラスはインスタンスに依存するため、このままではテストなどが正常に行えない。
そこで、このcase class User
をUserService
のコンパニオンオブジェクトに定義するように修正する。
object
はシングルトンであるから、そこに定義しているインナークラスがインスタンスに依存する問題を解消できる。
UserService.scala
object UserService { val users: Seq[(Int, String)] = Seq((1, "hoge"), (2, "fuga"), (3, "foo"), (4, "bar")) // case class をコンパニオンオブジェクトで定義する case class User(id: Int, name: String) } trait UserService { import UserService._ def getUsers: Seq[User] = users.map(u => User(id = u._1, name = u._2)) }
これを、先ほどと同様にsbt console
からインスタンス化しその値を比較しよう。
scala> val userController1 = new UserController userController1: UserController = UserController@3094148b scala> val userController2 = new UserController userController2: UserController = UserController@32fef5c6 scala> userController1.list == userController2.list res0: Boolean = true
今度は各インスタンスから呼び出したlist
メソッドの結果は同値となった。
このように、ScalaではTraitやclassの中にcase class
を定義すると、それらは実体化する際アウタークラスのインスタンスに依存するため、
かならずコンパニオンオブジェクトなど、object
のなかで定義することを心がけなければならない。