kotlin tips: 自己を参照される可能性のあるコールバックとか単一処理を複数のコールバックで扱うことについて
※とりあえず五月雨式にメモしておく。結論はまだない。
戻り値方式とクロージャ方式の例
下記の returnValue1.kt は、Model の関数が結果を戻り値で返しています。
// // returnValue1.kt // package returnValue1 import returnValue1.AppendResult.Correct import returnValue1.AppendResult.Wrong interface Model { val isCompleted: Boolean fun append(c: Char): AppendResult } sealed class AppendResult { class Correct(val isComplete: Boolean) : AppendResult() class Wrong : AppendResult() } class ModelImpl : Model { override var isCompleted = false private set private val inputLetters = StringBuilder() override fun append(c: Char): AppendResult { return when (c) { in '0'..'9' -> { inputLetters.append(c) Correct((inputLetters.length == 2).apply { isCompleted = this }) } else -> { Wrong() } } } } object view { fun onCorrectLetter(model: ModelImpl) { println("『ピッ!』") } fun onWrongLetter(model: ModelImpl) { println("『ブブーッ!』") } fun onComplete(model: ModelImpl) { println("『ピロリロリン!』[isCompleted = ${model.isCompleted}]") } } // controller fun main(args: Array<String>) { val model = ModelImpl() fun append(c: Char) { with(model.append(c)) { when (this) { is Correct -> { view.onCorrectLetter(model) if (isComplete) view.onComplete(model) } is Wrong -> view.onWrongLetter(model) } } } append('1') // 数字なので正常受付。『ピッ!』と鳴る。 append('a') // 数字じゃないのでエラーを表示。『ブブーッ!』っと鳴る。 append('2') // 数字なので正常受付。『ピッ!』と鳴る。入力完了音が『ピロリロリン!』と鳴る。 }
実行結果
『ピッ!』 『ブブーッ!』 『ピッ!』 『ピロリロリン!』[isCompleted = true]
下記の closure1.kt は、Model 関数にクロージャが渡され、結果をそのクロージャ経由で通知しています。
結果毎の処理を Model 側でキックしてくれるので、controller 側に条件分岐が必要無くなっています。
// // closure1.kt // package closure1 interface Model { val isCompleted: Boolean fun append(c: Char, onCorrectLetter: (model: ModelImpl) -> Unit, onWrongLetter: (model: ModelImpl) -> Unit, onComplete: (model: ModelImpl) -> Unit) } class ModelImpl : Model { override var isCompleted = false private set private val inputLetters = StringBuilder() override fun append(c: Char, onCorrectLetter: (model: ModelImpl) -> Unit, onWrongLetter: (model: ModelImpl) -> Unit, onComplete: (model: ModelImpl) -> Unit) { when (c) { in '0'..'9' -> { inputLetters.append(c) onCorrectLetter(this) if (inputLetters.length == 2) { isCompleted = true onComplete(this) } } else -> { onWrongLetter(this) } } } } object view { fun onCorrectLetter(model: ModelImpl) { println("『ピッ!』") } fun onWrongLetter(model: ModelImpl) { println("『ブブーッ!』") } fun onComplete(model: ModelImpl) { println("『ピロリロリン!』[isCompleted = ${model.isCompleted}]") } } // controller fun main(args: Array<String>) { val model = ModelImpl() model.append('1', view::onCorrectLetter, view::onWrongLetter, view::onComplete) // 数字なので正常受付。『ピッ!』と鳴る。 model.append('a', view::onCorrectLetter, view::onWrongLetter, view::onComplete) // 数字じゃないのでエラーを表示。『ブブーッ!』っと鳴る。 model.append('2', view::onCorrectLetter, view::onWrongLetter, view::onComplete) // 数字なので正常受付。『ピッ!』と鳴る。入力完了音が『ピロリロリン!』と鳴る。 }
実行結果
『ピッ!』 『ブブーッ!』 『ピッ!』 『ピロリロリン!』[isCompleted = true]
下記の closure2.kt は、Model にあらかじめクロージャが渡され、関数が結果をそのクロージャ経由で通知しています。
関数呼び出し毎に異なるクロージャを渡す必要がない場合はこちらのほうがさらにシンプルです。
// // closure2.kt // package closure2 interface Model { val isCompleted: Boolean fun append(c: Char) } class ModelImpl(val onCorrectLetter: (model: ModelImpl) -> Unit, val onWrongLetter: (model: ModelImpl) -> Unit, val onComplete: (model: ModelImpl) -> Unit) : Model { override var isCompleted = false private set private val inputLetters = StringBuilder() override fun append(c: Char) { when (c) { in '0'..'9' -> { inputLetters.append(c) onCorrectLetter(this) if (inputLetters.length == 2) { onComplete(this) isCompleted = true } } else -> { onWrongLetter(this) } } } } object view { fun onCorrectLetter(model: ModelImpl) { println("『ピッ!』") } fun onWrongLetter(model: ModelImpl) { println("『ブブーッ!』") } fun onComplete(model: ModelImpl) { println("『ピロリロリン!』[isCompleted = ${model.isCompleted}]") } } // controller fun main(args: Array<String>) { val model = ModelImpl(view::onCorrectLetter, view::onWrongLetter, view::onComplete) model.append('1') // 数字なので正常受付。『ピッ!』と鳴る。 model.append('a') // 数字じゃないのでエラーを表示。『ブブーッ!』っと鳴る。 model.append('2') // 数字なので正常受付。『ピッ!』と鳴る。入力完了音が『ピロリロリン!』と鳴る。 }
クロージャ方式にてバグを埋め込む可能性のある例
closure1.kt と closure2.kt については、Model が受け取るコールバック内部から自身が参照される場合、バグを埋め込む可能性があります。
具体的には、以下のようにコードを変更すると結果表示が変わってしまいます。
// 正しい順序 inputLetters.append(c) onCorrectLetter(this) if (inputLetters.length == 2) { isCompleted = true // isCompleted を true に変更してから onComplete(this) を呼び出している onComplete(this) } // 順序を変更してバグを埋め込む例 inputLetters.append(c) onCorrectLetter(this) if (inputLetters.length == 2) { onComplete(this) // この時点では isCompleted を true に変更する前に onComplete(this) を呼び出している isCompleted = true }
実行結果
『ピッ!』 『ブブーッ!』 『ピッ!』 『ピロリロリン!』[isCompleted = false]
returnValue1.kt のように戻り値を返す方式の場合、間違えようとしても間違えようがないので、コールバック方式よりも戻り値方式のほうが保守性が高そうです。
要件変更への耐性
余談になりますが、要件変更への耐性の例として、以下の要件変更を考えてみる。
- 完了時は『ピッ!』という音を出さずに『ピロリロリン!』だけを鳴らす。
戻り値方式の場合の変更点
1行削除でOK。
when (this) { is Correct -> { view.onCorrectLetter(model) // ← この1行を削除すればOK if (isComplete) view.onComplete(model) } is Wrong -> view.onWrongLetter(model) }
コールバック方式の場合
controller と model の両方に変更が発生する例。
// // closure3.kt // package closure3 interface Model { val isCompleted: Boolean fun append(c: Char) } // 変更前: class ModelImpl(val onCorrectLetter: (model: ModelImpl) -> Unit, val onWrongLetter: (model: ModelImpl) -> Unit, val onComplete: (model: ModelImpl) -> Unit) : Model { class ModelImpl(val onCorrectLetter: (isCompleted: Boolean, model: ModelImpl) -> Unit, val onWrongLetter: (model: ModelImpl) -> Unit) : Model { override var isCompleted = false private set private val inputLetters = StringBuilder() override fun append(c: Char) { when (c) { in '0'..'9' -> { inputLetters.append(c) // 変更前 // onCorrectLetter(this) // if (inputLetters.length == 2) { // onComplete(this) // isCompleted = true // } // 変更後 onCorrectLetter((inputLetters.length == 2).apply { isCompleted = this }, this) } else -> { onWrongLetter(this) } } } } object view { fun onCorrectLetter(model: ModelImpl) { println("『ピッ!』") } fun onWrongLetter(model: ModelImpl) { k println("『ブブーッ!』") } fun onComplete(model: ModelImpl) { println("『ピロリロリン!』[isCompleted = ${model.isCompleted}]") } } // controller fun main(args: Array<String>) { // 変更前 // val model = ModelImpl(view::onCorrectLetter, view::onWrongLetter, view::onComplete) // 変更後 val model = ModelImpl( { isCompleted, model -> when (isCompleted) { true -> view.onComplete(model) false -> view.onCorrectLetter(model) } }, view::onWrongLetter ) model.append('1') // 数字なので正常受付。『ピッ!』と鳴る。 model.append('a') // 数字じゃないのでエラーを表示。『ブブーッ!』っと鳴る。 model.append('2') // 数字なので正常受付。入力完了音が『ピロリロリン!』と鳴る。 }
controller 側のみに変更を局所化することも一応可能ではあるという例。
フラグ立てまくりでカオスwww
// // closure4.kt // package closure4 interface Model { val isCompleted: Boolean fun append(c: Char) } class ModelImpl(val onCorrectLetter: (model: ModelImpl) -> Unit, val onWrongLetter: (model: ModelImpl) -> Unit, val onComplete: (model: ModelImpl) -> Unit) : Model { override var isCompleted = false private set private val inputLetters = StringBuilder() override fun append(c: Char) { when (c) { in '0'..'9' -> { inputLetters.append(c) onCorrectLetter(this) if (inputLetters.length == 2) { onComplete(this) isCompleted = true } } else -> { onWrongLetter(this) } } } } object view { fun onCorrectLetter(model: Model) { println("『ピッ!』") } fun onWrongLetter(model: Model) { println("『ブブーッ!』") } fun onComplete(model: Model) { println("『ピロリロリン!』[isCompleted = ${model.isCompleted}]") } } // controller fun main(args: Array<String>) { var isCorrectLetter = false fun onCorrectLetter(model: Model) { isCorrectLetter = true } var isComplete = false fun onComplete(model: Model) { isComplete = true } val model = ModelImpl(::onCorrectLetter, view::onWrongLetter, ::onComplete) fun append(c: Char) { model.append(c) if (isCorrectLetter) { if (isComplete) view.onComplete(model) else view.onCorrectLetter(model) } isCorrectLetter = false isComplete = false } append('1') // 数字なので正常受付。『ピッ!』と鳴る。 append('a') // 数字じゃないのでエラーを表示。『ブブーッ!』っと鳴る。 append('2') // 数字なので正常受付。入力完了音が『ピロリロリン!』と鳴る。 }