Menu
CoddyTech

Mẫu Singleton

Thuộc phần Lập trình hướng đối tượng trong hành trình Kotlin của Coddy. Bài học 42 trên 57.

Mẫu thiết kế là giải pháp đã được kiểm chứng cho một vấn đề thiết kế thường xuyên xuất hiện. Mẫu singleton đảm bảo một lớp chỉ có đúng một instance mà toàn bộ chương trình có thể truy cập. Trong Kotlin, khai báo object là một singleton: nó được tạo một lần, khi dùng lần đầu, và an toàn ngay cả khi nhiều luồng khởi chạy cùng lúc:

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

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

Bên trong main:

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

Đầu ra:

editor in dark theme
14

Singleton lưu trữ trạng thái được các phần khác nhau của chương trình dùng chung. Hãy giữ trạng thái đó ở chế độ riêng tư và chỉ thay đổi thông qua các phương thức, để mọi thay đổi đều tuân theo cùng một quy tắc:

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")

Bên trong main:

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

Đầu ra:

home=2, about=1

Khi đối tượng duy nhất cần dữ liệu thiết lập, một lớp có private constructor có thể tạo đối tượng đó trong đối tượng đồng hành khi có yêu cầu đầu tiên và trả về cùng một đối tượng trong những lần sau:

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 }
    }
}

Bên trong main:

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

Đầu ra:

true
db://main

Chỉ dùng đối tượng đơn nhất (singleton) khi thật cần thiết. Đối tượng đơn nhất là trạng thái toàn cục: bất kỳ đoạn code nào cũng có thể thay đổi nó, khiến ta khó biết đoạn code nào phụ thuộc vào nó, đồng thời các bài kiểm thử không thể bắt đầu với một bản sao mới. Chúng phù hợp với những thứ thực sự chỉ có một bản trong mỗi chương trình, chẳng hạn như cấu hình hoặc bộ ghi nhật ký. Khi chỉ một phần của chương trình cần một đối tượng, hãy truyền đối tượng đó vào dưới dạng tham số có kiểu giao diện (interface), để bài kiểm thử có thể truyền vào một đối tượng giả thay thế:

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

class Session(private val clock: Clock) {      // Phần phụ thuộc hiển thị rõ
    val started = clock.now()
}
val session = Session(SystemClock)             // Một bài kiểm thử truyền vào Clock giả
challenge icon

Thử thách

Dễ

Hoàn thiện Logger dạng singleton. Nó có minLevel có thể thay đổi (debug, info, warn hoặc error, theo thứ tự đó). log(level, message) lưu [WARN] disk low khi cấp độ bằng hoặc cao hơn minLevel, nếu không thì tăng số lượng thông điệp trong dropped. Code bên ngoài có thể đọc nhưng không thể thay đổi giá trị này. entries là danh sách chỉ đọc chứa các dòng đã lưu, còn count(level) trả về số dòng đã lưu có cấp độ đó.

Các hàm trợ giúp trong Functions.kt đã được cung cấp: chúng truy cập cùng một Logger từ một tệp khác. Code được cung cấp đọc cấp độ tối thiểu, sau đó đọc các dòng level,message, rồi in ra từng mục và errors: 1, dropped: 2.

Code của bạn nằm trong Logger.kt và Functions.kt. Main.kt chứa code đầu vào/đầu ra đã được cung cấp và bạn không thể chỉnh sửa tệp này.

Tự làm thử

fun main() {
    // Đoạn code đầu vào/đầu ra đã có sẵn: giữ nguyên
    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 iconTự kiểm tra

Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.

Tất cả bài học trong Lập trình hướng đối tượng

Tự luyện tập: Playground Kotlin