Паттерны проектирования в Kotlin с примерами: продвинутый уровень
Паттерны проектирования — это проверенные решения типовых задач в разработке программного обеспечения. Если вы уже освоили базовые паттерны (Singleton, Factory, Observer), то пришло время перейти на следующий уровень. В этой статье мы разберём четыре продвинутых паттерна — Цепочка обязанностей, Интерпретатор, Посредник и Снимок — и покажем их реализацию на Kotlin с учётом идиом языка (sealed class, extension functions, лямбды).
1. Цепочка обязанностей (Chain of Responsibility)
Назначение: Позволяет передавать запрос последовательно по цепочке обработчиков, пока один из них не обработает запрос.
Проблема: Система логирования, где сообщения разного уровня (DEBUG, INFO, ERROR) должны обрабатываться разными логгерами.
Реализация на Kotlin:
// Абстрактный обработчик
abstract class Logger(private val level: Int) {
private var nextLogger: Logger? = null
fun setNext(logger: Logger): Logger {
this.nextLogger = logger
return logger
}
fun log(level: Int, message: String) {
if (this.level <= level) {
write(message)
}
nextLogger?.log(level, message)
}
protected abstract fun write(message: String)
}
// Конкретные обработчики
class ConsoleLogger(level: Int) : Logger(level) {
override fun write(message: String) = println("[Console] $message")
}
class FileLogger(level: Int) : Logger(level) {
override fun write(message: String) = println("[File] $message")
}
class EmailLogger(level: Int) : Logger(level) {
override fun write(message: String) = println("[Email] $message")
}
// Использование
fun main() {
val logger = ConsoleLogger(1).apply {
setNext(FileLogger(2))
.setNext(EmailLogger(3))
}
logger.log(2, "Предупреждение: низкий уровень памяти")
// Вывод:
// [Console] Предупреждение: низкий уровень памяти
// [File] Предупреждение: низкий уровень памяти
}
Преимущества в Kotlin: Благодаря apply и цепочечному вызову setNext код выглядит лаконично. Можно также заменить абстрактный класс на интерфейс с дефолтной реализацией.
2. Интерпретатор (Interpreter)
Назначение: Определяет грамматику языка и интерпретирует предложения этого языка.
Проблема: Необходимо разбирать и вычислять простые арифметические выражения (например, "3 + 5 - 2").
Реализация на Kotlin с sealed class:
// Абстрактное выражение
sealed class Expression {
abstract fun interpret(): Int
}
// Терминальное выражение — число
data class Number(val value: Int) : Expression() {
override fun interpret() = value
}
// Нетерминальные выражения — операции
data class Add(val left: Expression, val right: Expression) : Expression() {
override fun interpret() = left.interpret() + right.interpret()
}
data class Subtract(val left: Expression, val right: Expression) : Expression() {
override fun interpret() = left.interpret() - right.interpret()
}
// Парсер (упрощённый)
fun parse(tokens: List<String>): Expression {
val stack = mutableListOf<Expression>()
var i = 0
while (i < tokens.size) {
when (tokens[i]) {
"+" -> {
val right = Number(tokens[++i].toInt())
val left = stack.removeLast()
stack.add(Add(left, right))
}
"-" -> {
val right = Number(tokens[++i].toInt())
val left = stack.removeLast()
stack.add(Subtract(left, right))
}
else -> stack.add(Number(tokens[i].toInt()))
}
i++
}
return stack.first()
}
// Использование
fun main() {
val expression = parse(listOf("3", "+", "5", "-", "2"))
println(expression.interpret()) // 6
}
Преимущества в Kotlin: sealed class гарантирует исчерпывающий when и не требует else-ветки. Классы-данные (data class) автоматически предоставляют equals, hashCode и toString.
3. Посредник (Mediator)
Назначение: Уменьшает связанность между компонентами, централизуя общение через объект-посредник.
Проблема: Чат-комната, где пользователи отправляют сообщения друг другу, но не должны знать друг о друге напрямую.
Реализация на Kotlin с лямбдами:
// Посредник
class ChatMediator {
private val users = mutableMapOf<String, (String, String) -> Unit>()
fun register(name: String, onMessage: (String, String) -> Unit) {
users[name] = onMessage
}
fun sendMessage(from: String, to: String, message: String) {
users[to]?.invoke(from, message)
?: println("Пользователь $to не найден")
}
}
// Компонент
class User(private val mediator: ChatMediator, val name: String) {
fun send(to: String, message: String) {
mediator.sendMessage(name, to, message)
}
fun receive(from: String, message: String) {
println("[$name] получил от [$from]: $message")
}
fun register() {
mediator.register(name) { from, msg -> receive(from, msg) }
}
}
// Использование
fun main() {
val mediator = ChatMediator()
val alice = User(mediator, "Alice")
val bob = User(mediator, "Bob")
alice.register()
bob.register()
alice.send("Bob", "Привет, Боб!")
// Вывод: [Bob] получил от [Alice]: Привет, Боб!
}
Преимущества в Kotlin: Лямбды и функции высшего порядка делают реализацию посредника гибкой и компактной. Нет необходимости в интерфейсах — достаточно сигнатуры функции.
4. Снимок (Memento)
Назначение: Позволяет сохранять и восстанавливать внутреннее состояние объекта, не нарушая инкапсуляцию.
Проблема: Текстовый редактор с поддержкой Undo (отмена последнего действия).
Реализация на Kotlin:
// Снимок (Memento)
data class Memento(val state: String)
// Originator — объект, состояние которого сохра