Когда приложение разрастается, код может превратиться в запутанный клубок. Паттерны проектирования 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-классы и лямбды с получателем). Помните: паттерны не самоцель, они должны упрощать жизнь, а не усложнять чтение кода. Начните с тех, что решают вашу текущую задачу, и со временем освоите остальные.