Menu
CoddyTech

单例模式

本课属于 Coddy Kotlin 学习旅程的“面向对象编程”单元。第 42 课,共 57 课。

设计模式是针对反复出现的设计问题的一种经过验证的解决方案。单例模式可确保一个类只有一个实例,并且整个程序都能访问它。在 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

谨慎使用单例。单例是全局状态:任何代码都可以更改它,这会隐藏哪些代码依赖它,而且测试无法从全新的副本开始。单例适用于程序中确实只有一个实例的对象,例如配置或日志记录器。如果程序只有一部分需要某个对象,就把它作为参数传入,并将参数类型设为接口,这样测试就可以传入一个模拟对象:

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)。当级别不低于 minLevel 时,log(level, message) 会存储 [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 在线编辑器