概要
- あるインターフェイスを実装するクラスを作成する際に「委譲による実装」を利用すると楽になることがある。
- ただし公式リファレンスにある基本のやり方そのままだと、クラス内に隠蔽されるべきオブジェクトを、クラスの外から操作できてしまう場合がある。
- この問題はセカンダリコンストラクタを使った工夫をすることで回避できる。
課題
Fizz Buzz を行うクラスを実装してみましょう。
次のようなクラスです。
-
List<String>
インターフェイスを実装しており、["1", "2", "Fizz", "4", "Buzz", ...]
というような要素を持つ。 - 要素は最初は空であり、
next()
を呼び出すごとに1つずつ増える。
[]
—next()
→["1"]
—next()
→["1", "2"]
—next()
→ ...
使用例は次のようになります。
fun main() {
val fizzBuzz = FizzBuzz()
repeat(15) { fizzBuzz.next() } // 15までの要素を生成する。
fizzBuzz.forEach { print("$it, ") } // > 1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz,
}
どのように実装するとよいでしょうか。
あらかじめ、与えられた数値を Fizz Buzz した文字列に変換する関数だけ作っておきましょう。
fun fizzBuzz(number: Int): String = when {
number % 3 == 0 && number % 5 == 0 -> "FizzBuzz"
number % 3 == 0 -> "Fizz"
number % 5 == 0 -> "Buzz"
else -> number.toString(10)
}
失敗例
案1
「List<String>
を実装しとるクラスを継承したろ!」
class FizzBuzz1 : ArrayList<String>() { // List<String> インターフェイスを実装した ArrayList<String> クラスを継承。
fun next() {
this += fizzBuzz(size + 1)
}
}
「あかん、よそから勝手な値を突っ込めてまう」
fun main() {
val fizzBuzz = FizzBuzz1()
fizzBuzz += "FooBar"
fizzBuzz.forEach { print("$it, ") } // > FooBar,
}
案2
「ほな、 List<String>
インターフェイスを実装しよか」
class FizzBuzz2 : List<String> {
}
「List<String>
のメンバを全部自前で実装するんはさすがに面倒やな。なかで mutableListOf()
使(つこ)たろか」
class FizzBuzz2 : List<String> {
private val mutableList: MutableList<String> = mutableListOf()
}
「List<String>
インターフェイスのメンバが呼ばれたら mutableList
の同名のメンバを呼ぶようにすればええ。こないなふうに…」
class FizzBuzz2 : List<String> {
private val mutableList: MutableList<String> = mutableListOf()
override val size: Int
get() = mutableList.size
override fun get(index: Int): String = mutableList.get(index)
...
}
「…ってちょお待てや、何個あんねん!1 やっとれるかい!」
委譲による実装
こういうときに、「委譲による実装」機能が役に立ちます。
案3
class FizzBuzz3(
private val mutableList: MutableList<String> // コンストラクタ引数で MutableList<String> のインスタンスを受け取る。
): List<String> by mutableList { // List<String> のメンバが呼び出されたら mutableList の同じメンバに処理を委譲することを宣言。
fun next() {
mutableList += fizzBuzz(size + 1)
}
}
コンストラクタ引数で List<String>
インターフェイスを継承した MutableList<String>
のインスタンス mutableList
を受け取り、
クラスが List<String>
インターフェイスを実装していることを宣言しているところに by mutableList
を付けています。
こうすることで、List<String>
のメンバの実装として mutableList
が使用されます。(案2でやろうとしたように。)
案3の欠点
上記は公式リファレンスにも書かれているやりかたです。
ですが…。
これは mutableList
オブジェクトをコンストラクタ引数で受け取っています。
つまり、外から mutableList
を、ひいては FizzBuzz3
オブジェクトを、操作することができてしまうのです。
fun main() {
val mutableList = mutableListOf<String>()
val fizzBuzz = FizzBuzz3(mutableList)
mutableList += "FooBar"
fizzBuzz.forEach { print("$it, ") } // > FooBar,
}
(あと、使う側が mutableList
を用意してやらないといけないのがめんどくさい。)
案4(完成)
これを解決するにはどうしたらよいか。
- プライマリコンストラクタを
private
にし、 - セカンダリコンストラクタで
mutableList
を生成してプライマリコンストラクタに渡しましょう。
(コンストラクタの書き方について詳しくない方はこちらをご覧ください。→【Kotlin】コンストラクタ − プライマリとセカンダリ)
class FizzBuzz private constructor(
private val mutableList: MutableList<String>
) : List<String> by mutableList {
constructor() : this(mutableListOf())
fun next() {
mutableList += fizzBuzz(size + 1)
}
}
これで mutableList
をクラス内に隠蔽し、外部から操作されることがなくなりました!
(使う側が mutableList
を用意する必要もなくなりました!)
-
11個 ↩