Menu
Coddy logo textTech

Паттерн «Одиночка»

Часть раздела Объектно-ориентированное программирование путешествия по Kotlin на Coddy. Урок 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, именно в таком порядке). log(level, message) сохраняет [WARN] disk low, если уровень не ниже minLevel, а в противном случае учитывает сообщение в 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