初めてRubyからscalaに来たとき、「なんで特異クラスっぽい? シングルトンパターンっぽい?やつがいっぱいいるの?」と悩みました。なぜならrubyのクラス変数やシングルトンパターンはアンチパターン扱いされるほど嫌われていたからです。
しかし、 scalaのコンパニオンオブジェクトとrubyの特異クラスは見かけ上似ていますが、意味が違います。
この記事ではRubyエンジニア目線で、Scalaのコンパニオンオブジェクトについて解説していきたいと思います。
コンパニオンオブジェクトとは?
「コンパニオンオブジェクト」とは、あるクラスに対して同じスコープ、同じ名前で定義されたシングルトンオブジェクトです。
class Person
object Person
これがコンパニオンオブジェクトです。
が、シングルトンオブジェクトに対して漠然とした不安を感じます。。
コンパニオンオブジェクトはrubyのintializeみたいなもん
しかし、その不安はすぐに安心に変わります。
なぜなら、scalaのコンパニオンオブジェクトは、Rubyのinitializeみたいなものであり、大抵のクラスにおいて必要な機能だからです。
例えばRubyだとインスタンスを生成する際にこう書きます。
class Person
def initialize(name)
@id = 1000
@name = name
end
end
Person.new("マイケル")
しかし、scala(静的型付き言語)でRubyのノリで書こうとすると...
class Person(id: Int, val name: String)
object Main extends App {
new Person("マイケル")
}
//-> コンパイルエラー
型の制約があるので、コンパイルエラーが出てしまいます。
rubyはinitializeだけで、外側からメンバーの値を与えてあげなくても、メンバを持たせることができます。
しかし、静的型付き言語ではクラスに型を定義する必要があるので(Rubyと違うところ)、 型を定義されたクラスと、クラスを生成するものが必要となります。それがコンパニオンオブジェクトです。
scalaではこう書きます。
class Person(id: Int, name: String)
object Person {
def apply(name: String):Person = {
val id = 1000
new Person(id, name)
}
}
object Main extends App {
// Person.apply("マイケル")
// apply関数は省略できる
Person("マイケル")
}
この段階で、コンパニオンオブジェクトの必要性がわかりました。
しかし、このままだとプログラム上まだ問題があります。
それは、複数の方法でインスタンスを生成できてしまうことです。
object Main extends App {
val person1 = Person("マイケル")
val person2 = new Person(2000, "ボブ")
}
そこで、Personクラスの引数(コンストラクタ引数)をprivateにして、person2の記法を封印したいと思います。
// コンストラクタ引数をprivateにした
class Person private (id: Int, name: String)
object Person {
def apply(name: String):Person = {
val id = 1000
new Person(id, name)
}
}
object Main extends App {
val person1 = Person("マイケル")
val person2 = new Person(2000, "ボブ")
}
// -> person2を生成しようとするとコンパイルエラー
Memberクラスのidとnameはprivateなので外からアクセスすることができません。
しかし、 コンパニオンオブジェクトのコンテキストではコンパニオンクラスのprivateにアクセスすることができます。
これがコンパニオンオブジェクトの特徴です。
classとコンパニオンオブジェクトに定義するべきものはなに??
簡単に言うと、Rubyでクラスに対して定義していたものは、コンパニオンオブジェクト、Rubyでインスタンスに対して定義していたものはクラスに定義します。
つまり、
class Person
DEFAULT_ID = 1000
class << self
def find_by_name(name)
# do something
end
end
def initialize(name)
@id = DEFAULT_ID
@name = name
end
def name_with_sama
self.name + '様'
end
end
をscalaで書くと...
class Person(id: Int, name: String){
val name_with_sama:String = name + "様"
}
object Person {
val default_id = 1000
def apply(name):Person = {
new Person(default_id, name)
}
def findByName(name):Person = {
// do something
}
}
まとめ
- 外から渡したくないメンバをもったクラスを作るときに、コンパニオンオブジェクトを使う。
- コンパニオンオブジェクトのコンテキストではコンパニオンクラスのprivateにアクセスすることができる。
- クラスに対して定義するものはコンパニオンオブジェクト、インスタンスに対して定義するものはクラス。