blog.petitviolet.net

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

2017-09-26

QiitaScala

この記事はなに?

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")

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

from: https://qiita.com/petitviolet/items/b6af2877f64ebe8fe312