Singleton Pattern
Part of the Object Oriented Programming section of Coddy's Kotlin journey. Lesson 42 of 57.
A design pattern is a proven solution to a design problem that comes up again and again. The singleton pattern makes sure a class has exactly one instance that the whole program can reach. In Kotlin an object declaration is a singleton: it is created once, on first use, and safely even when several threads start at the same time:
object Settings {
var theme = "light"
var fontSize = 14
}
fun openEditor() = "editor in ${Settings.theme} theme"Inside main:
Settings.theme = "dark"
println(openEditor())
println(Settings.fontSize)Output:
editor in dark theme
14The singleton holds state that different parts of the program share. Keep that state private and change it only through methods, so every change goes through the same rules:
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")Inside main:
home(); about(); home()
println(Metrics.report())Output:
home=2, about=1When the single instance needs setup data, a class with a private constructor can create it in its companion object on the first request and return the same instance afterwards:
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 }
}
}Inside main:
val a = Database.connect("db://main")
val b = Database.connect("db://other")
println(a === b)
println(b.url)Output:
true
db://mainUse singletons sparingly. A singleton is global state: any code can change it, which hides who depends on it, and tests cannot start from a fresh copy. They fit what is truly one per program, such as the configuration or a logger. When only a part of the program needs an object, pass it in as a parameter, typed as an interface, so a test can pass a fake instead:
interface Clock { fun now(): Long }
object SystemClock : Clock { override fun now() = System.currentTimeMillis() }
class Session(private val clock: Clock) { // the dependency is visible
val started = clock.now()
}
val session = Session(SystemClock) // a test passes a fake ClockChallenge
EasyComplete the singleton Logger. It has a changeable minLevel (debug, info, warn or error, in that order). log(level, message) stores [WARN] disk low when the level is at least minLevel and otherwise counts the message in dropped, which outside code can read but not change. entries is a read-only list of the stored lines, and count(level) returns how many stored lines have that level.
The helper functions in Functions.kt are supplied: they reach the same Logger from another file. The supplied code reads the minimum level and then lines level,message, and prints every entry and errors: 1, dropped: 2.
Your code goes in Logger.kt and Functions.kt. Main.kt holds the supplied input/output code and cannot be edited.
Try it yourself
fun main() {
// Supplied input/output code: keep it as it is
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}")
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Classes and Objects
Working With FilesDefining ClassesPrimary Constructorsinit Blocks and ValidationSecondary ConstructorsRecap - Library Card4Inheritance
Open Classes and SubclassesOverriding Memberssuper and ConstructorsAbstract ClassesRecap - Staff Directory2Properties and Encapsulation
Custom AccessorsVisibility ModifiersPrivate Setterslazy and lateinitRecap - Bank Account5Interfaces
Declaring InterfacesDefault MethodsMultiple InterfacesInterfaces as TypesRecap - Payment Methods8Generics
Generic FunctionsGeneric ClassesType ConstraintsVariance With in and outRecap - Priority Queue11Project: Library Management
Books and MembersBorrowing BooksPractice on your own: Kotlin playground