Menu
Coddy logo textTech

Singletonパターン

CoddyのKotlinジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 42/57。

デザインパターンとは、何度も繰り返し発生する設計上の問題に対する、実証済みの解決策です。シングルトンパターンは、プログラム全体からアクセスできるインスタンスをクラスが正確に1つだけ持つようにします。Kotlinでは、object宣言がシングルトンです。これは初回使用時に一度だけ作成され、複数のスレッドが同時に開始した場合でも安全です。

object Settings {
    var theme = "light"
    var fontSize = 14
}

fun openEditor() = "editor in ${Settings.theme} theme"

mainの内部で:

Settings.theme = "dark"
println(openEditor())
println(Settings.fontSize)

出力:

editor in dark theme
14

シングルトンは、プログラムのさまざまな部分で共有する状態を保持します。その状態は非公開にし、メソッドを通してのみ変更するようにします。そうすれば、すべての変更が同じルールに従います。

object Metrics {
    private val hits = mutableMapOf<String, Int>()

    fun hit(page: String) { hits[page] = (hits[page] ?: 0) + 1 }
    fun report() = hits.entries.joinToString { "${it.key}=${it.value}" }
}

fun home() = Metrics.hit("home")
fun about() = Metrics.hit("about")

main の中で:

home(); about(); home()
println(Metrics.report())

出力:

home=2, about=1

単一のインスタンスにセットアップデータが必要な場合、private constructorを持つクラスは、最初のリクエスト時にコンパニオンオブジェクト内でそれを作成し、その後は同じインスタンスを返すことができます。

class Database private constructor(val url: String) {
    companion object {
        private var instance: Database? = null

        fun connect(url: String): Database =
            instance ?: Database(url).also { instance = it }
    }
}

mainの中で:

val a = Database.connect("db://main")
val b = Database.connect("db://other")
println(a === b)
println(b.url)

出力:

true
db://main

シングルトンは慎重に使用してください。シングルトンはグローバル状態です。どのコードからでも変更できるため、何がそれに依存しているのかが分かりにくくなり、テストを新しいコピーから開始できません。設定やロガーなど、本当にプログラムごとに1つだけ存在するものには適しています。プログラムの一部だけがオブジェクトを必要とする場合は、インターフェース型のパラメーターとして渡してください。そうすれば、テストで代用品を渡せます。

interface Clock { fun now(): Long }
object SystemClock : Clock { override fun now() = System.currentTimeMillis() }

class Session(private val clock: Clock) {      // 依存関係が見える
    val started = clock.now()
}
val session = Session(SystemClock)             // テストは偽のClockを渡す
challenge icon

チャレンジ

簡単

シングルトン Logger を完成させてください。変更可能な minLevel(順に debug、info、warn、error)を持ちます。log(level, message) は、レベルが minLevel 以上の場合に [WARN] disk low を保存し、それ以外の場合はメッセージを dropped にカウントします。dropped はコードの外部から読み取れますが、変更はできません。entries は保存された行の読み取り専用リストであり、count(level) はそのレベルを持つ保存済みの行数を返します。

Functions.kt のヘルパー関数は用意されています。これらは別のファイルから同じ Logger にアクセスします。用意されたコードは最小レベルを読み取り、続いて level,message の形式の行を読み取り、すべてのエントリと errors: 1, dropped: 2 を出力します。

コードは Logger.kt と Functions.kt に記述します。Main.kt には用意された入力/出力コードが含まれており、編集できません。

自分で試してみよう

fun main() {
    // 提供された入出力コード: そのままにしておく
    val input = generateSequence(::readLine).toList()
    Logger.minLevel = input[0]
    for (line in input.drop(1)) {
        val (level, message) = line.split(",", limit = 2)
        when (level) {
            "debug" -> debug(message)
            "info" -> info(message)
            "warn" -> warn(message)
            else -> error(message)
        }
    }
    for (e in Logger.entries) println(e)
    println("errors: ${Logger.count("error")}, dropped: ${Logger.dropped}")
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン

自分で練習してみよう: Kotlinプレイグラウンド