Menu
Coddy logo textTech

Singleton-Muster

Teil des Abschnitts Objektorientierte Programmierung der Kotlin-Journey von Coddy. Lektion 42 von 57.

Ein Entwurfsmuster ist eine bewährte Lösung für ein Entwurfsproblem, das immer wieder auftritt. Das Singleton-Muster stellt sicher, dass eine Klasse genau eine Instanz besitzt, die das gesamte Programm erreichen kann. In Kotlin ist eine object-Deklaration ein Singleton: Sie wird bei der ersten Verwendung einmal erstellt und ist auch dann sicher, wenn mehrere Threads gleichzeitig starten:

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

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

Innerhalb von main:

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

Ausgabe:

editor in dark theme
14

Das Singleton enthält einen Zustand, den verschiedene Teile des Programms gemeinsam nutzen. Halte diesen Zustand privat und ändere ihn nur über Methoden, damit jede Änderung denselben Regeln folgt:

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

Innerhalb von main:

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

Ausgabe:

home=2, about=1

Wenn die einzelne Instanz Einrichtungsdaten benötigt, kann eine Klasse mit einem private constructor sie bei der ersten Anfrage in ihrem Companion-Objekt erstellen und danach dieselbe Instanz zurückgeben:

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

Innerhalb von main:

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

Ausgabe:

true
db://main

Verwende Singletons sparsam. Ein Singleton ist ein globaler Zustand: Jeder Code kann ihn ändern, wodurch verborgen bleibt, wer von ihm abhängt, und Tests können nicht mit einer frischen Kopie beginnen. Sie eignen sich für Dinge, die tatsächlich nur einmal pro Programm vorkommen, etwa die Konfiguration oder einen Logger. Wenn nur ein Teil des Programms ein Objekt benötigt, übergib es als Parameter, typisiert als Interface, damit ein Test stattdessen ein Fake übergeben kann:

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

class Session(private val clock: Clock) {      // die Abhängigkeit ist sichtbar
    val started = clock.now()
}
val session = Session(SystemClock)             // ein Test übergibt eine gefälschte Clock
challenge icon

Aufgabe

Einfach

Vervollständige das Singleton Logger. Es verfügt über eine änderbare minLevel (debug, info, warn oder error, in dieser Reihenfolge). log(level, message) speichert [WARN] disk low, wenn die Ebene mindestens minLevel entspricht, und zählt die Nachricht andernfalls in dropped, das außerhalb des Codes gelesen, aber nicht geändert werden kann. entries ist eine schreibgeschützte Liste der gespeicherten Zeilen, und count(level) gibt zurück, wie viele gespeicherte Zeilen diese Ebene haben.

Die Hilfsfunktionen in Functions.kt werden bereitgestellt: Sie greifen von einer anderen Datei aus auf dasselbe Logger zu. Der bereitgestellte Code liest die minimale Ebene und anschließend Zeilen im Format level,message ein und gibt jeden Eintrag sowie errors: 1, dropped: 2 aus.

Dein Code gehört in Logger.kt und Functions.kt. Main.kt enthält den bereitgestellten Eingabe-/Ausgabecode und kann nicht bearbeitet werden.

Probier es selbst

fun main() {
    // Gelieferter Ein-/Ausgabecode: belasse ihn wie er ist
    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 iconTeste dich selbst

Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.

Alle Lektionen in Objektorientierte Programmierung

Übe selbstständig: Kotlin-Playground