Паттерны проектирования в Kotlin: продвинутые техники с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Паттерны проектирования в 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 — объект, состояние которого сохра                

Блоги

Рекомендаций по книгам