Scala - case classのコンストラクタを隠蔽する
2017-09-26
QiitaScalaこの記事はなに?
case class を使いながらコンストラクタを隠蔽してファクトリー経由で生成するように強制したい。
ここでいう"コンストラクタ"はnew
で生成する通常コンストラクタに加えて case class で自動生成されるapply
も対象としている。
まとめ
sealed abstract case class
を使う- こちらのコメントをご参照下さい(@aoiroaoino さんありがとうございます)
- きっちり隠蔽したければ case class は諦めて class を使う
- 全部自前で実装するなら
- コードレビューで頑張れるなら case class 使う
copy
とかが欲しければこちら
- 色々と頑張れるならメタプロ
case class について
Scala の case class は非常に便利で、apply
やunapply
を自動で生やしてくれる。
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")
ここまでやるかどうかも、チーム次第。
from: https://qiita.com/petitviolet/items/b6af2877f64ebe8fe312