[Scala]case classのコンストラクタを隠蔽する

More than 1 year has passed since last update.


この記事はなに?

case classを使いながらコンストラクタを隠蔽してファクトリー経由で生成するように強制したい。

ここでいう"コンストラクタ"はnewで生成する通常コンストラクタに加えてcase classで自動生成されるapplyも対象としている。


まとめ



  • sealed abstract case classを使う



  • きっちり隠蔽したければcase classは諦めてclassを使う


    • 全部自前で実装するなら



  • コードレビューで頑張れるならcase class使う



    • copyとかが欲しければこちら



  • 色々と頑張れるならメタプロ


case classについて

Scalaのcase classは非常に便利で、applyunapplyを自動で生やしてくれる。

case class User (id: Int, name: String)

case classについては普通にコンストラクタ経由でもapply経由でも生成できる。

val alice = new User(1, "alice")

val bob = User.apply(2, "bob")

ちなみにscala -Xprint:typerで生成されるapplyメソッドを見てみると以下のようになっている。


単にnewしてるだけ。

case <synthetic> def apply(id: Int, name: String): User = new User(id, name);


コンストラクタを隠す


コンストラクタをprivateにする

classはコンストラクタの前にprivateをつけるとコンストラクタを隠蔽できる。

case class User private (id: Int, name: String)

こうするとコンストラクタは隠せるのでnew出来ない。

val alice = new User(1, "alice")

エラーメッセージはこんな感じ。


scala> new User(1, "alice")

:14: error: constructor User in class User cannot be accessed in object $iw


new User(1, "alice")


が、User.applyは生成されているので自由に生成できてしまう。

val bob = User.apply(2, "bob")


applyをoverrideしてprivateにする

コンストラクタをprivateにするのとあわせて、case classで自動生成されるapplyを明示的にprivateとして実装してみる。

case class User private (id: Int, name: String)

object User { private def apply(id: Int, name: String) = ??? }

こうすると

val alice = new User(1, "alice")

val bob = User.apply(2, "bob")

どちらもコンパイルエラーとなってめでたし。

...とはならず、Userクラスにフィールドが増えた場合などにapplyのシグネチャを変更し忘れるとすり抜けてしまう。

// ageが増えた

case class User private (id: Int, name: String, age: Int)
// ↓は先ほどと同じ
object User { private def apply(id: Int, name: String) = ??? }

こうなると、以下のようになる

// これはコンパイルエラー

// val alice = new User(1, "alice")
// こちらは動く
val bob = User.apply(2, "bob", 20)

コードレビューで人間が目で見て頑張って防ぐこととなる。


じゃあどうする?

case classを諦めてclassを使う。

class User private (id: Int, name: String)

とするだけでコンストラクタは適切に隠蔽できる。


case classのunapplyとかcopyとか、そのあたりの便利さとクラス定義としての厳密さのどちらを取るかはチーム次第、というありきたりな結論。


(番外編)applyを自動でprivateにする

人間が頑張るんじゃなくて自動でやりたい。

そうなるとメタプログラミングの出番。

自動でapplyを生成してprivateにしてくれるやつを作ってみた。


scala-acase/NoApply.scala

@NoApplyアノテーションを付与する。

@NoApply case class User private (id: Int, name: String)

そうすると

// どちらもコンパイルエラーになる

// new User(1, "alice")
// User.apply(2, "bob")

のどちらもコンパイルが通らなくなる。

なので、意図通りにファクトリー経由での生成を強制できる。

@NoApply case class User private (id: Int, name: String)

object User {
def create(name: String) = apply(Random.nextInt(100), name)
}

User.create("alice") // User(xxx, "alice")

ここまでやるかどうかも、チーム次第。