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

Когда приложение разрастается, код может превратиться в запутанный клубок. Паттерны проектирования Kotlin — это проверенные временем приёмы, которые помогают навести порядок в классах и связях. В этой статье я простыми словами объясню самые востребованные паттерны и покажу, как их применять на Kotlin, используя сильные стороны языка: объектные объявления, data-классы, лямбды и функции-расширения.


Что такое паттерны и зачем они нужны

Паттерн проектирования — это не готовый код, а шаблон решения типичной проблемы. Представьте, что вы готовите пиццу: последовательность действий всегда одна — тесто, начинка, сыр, выпечка. Это паттерн «Строитель». В программировании такие же шаблоны помогают не изобретать колесо и писать код, который легко читать, тестировать и менять.

В Kotlin многие классические паттерны реализуются лаконичнее, чем в Java, благодаря особенностям языка. Например, ключевое слово object заменяет целый класс для Singleton, а функции-расширения позволяют добавлять методы без наследования. Давайте разберём четыре самых частых паттерна, которые встречаются в Android- и серверной разработке.


Singleton: один экземпляр на всё приложение

Иногда нужно, чтобы объект был в единственном экземпляре: настройки приложения, база данных, логгер. Паттерн Одиночка гарантирует, что у класса будет ровно один экземпляр, и предоставляет глобальную точку доступа к нему. В Java для этого писали приватный конструктор и статический метод, в Kotlin всё заменяет ключевое слово object.

object AppConfig {
    var serverUrl: String = "https://api.example.com"
    var isDebugMode: Boolean = false

    fun printConfig() {
        println("Server: $serverUrl, Debug: $isDebugMode")
    }
}

// использование
AppConfig.serverUrl = "https://myapp.com"
AppConfig.printConfig()

object лениво создаётся при первом обращении, потокобезопасен, и вам не нужно думать о деталях. Это самый простой и рекомендуемый способ реализации Singleton в Kotlin.


Фабрика: создание объектов без привязки к конкретному классу

Когда объект нельзя создать напрямую через конструктор — например, он требует сложной настройки или вы должны выбрать один из нескольких подклассов, — на помощь приходит Фабрика. Она прячет логику создания за простым методом. В Kotlin для этого часто используют companion object, который играет роль статического члена класса.

interface Animal {
    fun voice(): String
}

class Dog : Animal {
    override fun voice() = "Гав"
}

class Cat : Animal {
    override fun voice() = "Мяу"
}

class AnimalFactory {
    companion object {
        fun createAnimal(type: String): Animal = when (type.lowercase()) {
            "dog" -> Dog()
            "cat" -> Cat()
            else -> throw IllegalArgumentException("Неизвестный тип")
        }
    }
}

val pet = AnimalFactory.createAnimal("dog")
println(pet.voice()) // Гав

Фабрика отделяет клиентский код от конкретных реализаций. Если позже добавится новый тип животного, вы измените только фабрику, а все места вызова createAnimal останутся нетронутыми.


Наблюдатель: подписка на события

Паттерн Наблюдатель нужен, когда одни объекты должны реагировать на изменения в других, но при этом они не знают друг о друге. Самый яркий пример — кнопка и слушатель клика. В классическом виде паттерн требует отдельный интерфейс и управление подписками, но в Kotlin можно использовать лямбды и Flow.

Простейший случай — реализация через лямбду:

class EventSource {
    private val listeners = mutableListOf<(String) -> Unit>()

    fun addListener(listener: (String) -> Unit) {
        listeners.add(listener)
    }

    fun fireEvent(message: String) {
        listeners.forEach { it(message) }
    }
}

val source = EventSource()
source.addListener { message -> println("Получено: $message") }
source.fireEvent("Привет!")

Если нужно управлять жизненным циклом или отменять подписки, используют Kotlin Flow в сочетании с корутинами. Это более мощный и современный механизм, особенно для Android, где подписки должны отменяться при уходе с экрана.


Строитель: пошаговое создание сложного объекта

Когда объект имеет много необязательных параметров, конструктор с десятком аргументов становится нечитаемым. Паттерн Строитель позволяет собирать объект пошагово, указывая только нужные характеристики. В Kotlin эту роль часто выполняют data-классы с именованными параметрами и методом copy.

data class User(
    val name: String,
    val age: Int? = null,
    val email: String? = null,
    val phone: String? = null
)

// использование — напоминает Builder
val user = User(
    name = "Иван",
    age = 30,
    email = "ivan@example.com"
    // phone не указываем, он будет null
)

Если логика создания сложнее, можно реализовать Builder вручную с помощью лямбды с получателем:

class UserBuilder {
    var name: String = ""
    var age: Int? = null
    var email: String? = null

    fun build(): User = User(name, age, email)
}

fun user(block: UserBuilder.() -> Unit): User = UserBuilder().apply(block).build()

val builtUser = user {
    name = "Мария"
    age = 25
    email = "maria@example.com"
}

Такой подход даёт гибкость классического Строителя, но сохраняет код Kotlin-идиоматичным.


Краткий обзор других паттернов

Кроме четырёх основных, в Kotlin можно легко реализовать многие другие паттерны. Вот несколько примеров с идиоматичным кодом:

  • Декоратор. Используйте функции-расширения, чтобы добавить новое поведение существующим классам без изменения их кода.
  • Стратегия. Функциональный тип (A) -> B позволяет передавать поведение как параметр без создания отдельного интерфейса.
  • Итератор. Расширьте класс методом operator fun iterator(), и его можно будет перебирать в цикле for — итератор внутри объекта спрятан в стандартной библиотеке.
  • Прототип. Data-классы уже имеют метод copy(), который создаёт копию с возможностью изменить отдельные поля.
  • Адаптер. Функция-расширение, «оборачивающая» вызов одного класса в интерфейс другого, часто заменяет целый класс-адаптер.

Практика: объединяем паттерны в простое приложение

Чтобы закрепить материал, напишем игрушечный пример — мини-сервис для отправки уведомлений. Singleton будет хранить настройки, Фабрика создавать каналы отправки, Наблюдатель оповещать об успешной доставке, а Строитель собирать сложное сообщение.

// Singleton для конфигурации
object NotificationConfig {
    var defaultSender: String = "noreply@myapp.com"
    var maxRetries: Int = 3
}

// Фабрика каналов
interface NotificationChannel {
    fun send(message: String)
}

class EmailChannel : NotificationChannel {
    override fun send(message: String) = println("Email: $message")
}

class SmsChannel : NotificationChannel {
    override fun send(message: String) = println("SMS: $message")
}

object ChannelFactory {
    fun getChannel(type: String): NotificationChannel = when (type) {
        "email" -> EmailChannel()
        "sms" -> SmsChannel()
        else -> throw IllegalArgumentException("Неизвестный канал")
    }
}

// Наблюдатель
class NotificationService {
    private val listeners = mutableListOf<(String) -> Unit>()
    fun onSuccess(listener: (String) -> Unit) = listeners.add(listener)

    fun send(message: String, channel: NotificationChannel) {
        channel.send(message)
        listeners.forEach { it(message) }
    }
}

// Строитель сообщения
class MessageBuilder {
    var title: String = ""
    var body: String = ""
    var recipients: List<String> = emptyList()
    fun build() = "$title: $body (кому: $recipients)"
}

fun message(block: MessageBuilder.() -> Unit) = MessageBuilder().apply(block).build()

// использование
val message = message {
    title = "Важное уведомление"
    body = "Обновление успешно установлено"
    recipients = listOf("user@example.com")
}

val service = NotificationService()
service.onSuccess { msg -> println("Успешно: $msg") }
service.send(message, ChannelFactory.getChannel("email"))

Итоги

Паттерны проектирования Kotlin не просто теория, а практический инструмент для написания поддерживаемого и расширяемого кода. Вы познакомились с четырьмя самыми частыми шаблонами: Singleton (через object), Factory (через companion object), Observer (через лямбды и Flow), Builder (через data-классы и лямбды с получателем). Помните: паттерны не самоцель, они должны упрощать жизнь, а не усложнять чтение кода. Начните с тех, что решают вашу текущую задачу, и со временем освоите остальные.