Введение в паттерны проектирования в Kotlin
Паттерны проектирования (design patterns) — это проверенные решения типовых задач разработки. В языке Kotlin многие классические паттерны реализуются более лаконично и безопасно благодаря встроенным возможностям: null-безопасности, функциям-расширениям, корутинам и мощной системе типов.
В этой статье мы разберём три группы паттернов: порождающие, структурные и поведенческие — с реальными примерами на Kotlin. Вы узнаете, как использовать их в повседневной разработке для создания гибкой и поддерживаемой архитектуры.
Порождающие паттерны (Creational)
1. Singleton (Одиночка)
В Kotlin одиночка реализуется ключевым словом object. Это потокобезопасный ленивый синглтон без лишнего кода.
object DatabaseConnection {
private val url = "jdbc:postgresql://localhost:5432/mydb"
private val user = "admin"
private val password = "secret"
fun connect(): String {
return "Connected to $url as $user"
}
}
fun main() {
println(DatabaseConnection.connect())
// Проверяем, что это один и тот же объект
val ref1 = DatabaseConnection
val ref2 = DatabaseConnection
println(ref1 === ref2) // true
}
Преимущество: не нужно писать synchronized или volatile — Kotlin гарантирует единственный экземпляр.
2. Factory Method (Фабричный метод)
Позволяет делегировать создание объектов подклассам. В Kotlin удобно использовать sealed class и companion object.
sealed class Notification {
data class Email(val to: String, val subject: String) : Notification()
data class SMS(val phone: String, val message: String) : Notification()
data class Push(val deviceId: String, val title: String) : Notification()
companion object Factory {
fun createEmail(to: String, subject: String): Notification = Email(to, subject)
fun createSMS(phone: String, message: String): Notification = SMS(phone, message)
fun createPush(deviceId: String, title: String): Notification = Push(deviceId, title)
}
}
fun send(notification: Notification) {
when (notification) {
is Notification.Email -> println("Email to ${notification.to}")
is Notification.SMS -> println("SMS to ${notification.phone}")
is Notification.Push -> println("Push to ${notification.deviceId}")
}
}
fun main() {
val email = Notification.createEmail("user@example.com", "Hello")
send(email)
}
3. Builder (Строитель)
В Kotlin Builder можно заменить именованными аргументами и значениями по умолчанию. Но для сложных объектов с валидацией классический Builder остаётся актуальным.
class Pizza private constructor(
val size: String,
val cheese: Boolean,
val pepperoni: Boolean,
val mushrooms: Boolean
) {
class Builder {
private var size: String = "Medium"
private var cheese: Boolean = false
private var pepperoni: Boolean = false
private var mushrooms: Boolean = false
fun size(size: String) = apply { this.size = size }
fun cheese() = apply { this.cheese = true }
fun pepperoni() = apply { this.pepperoni = true }
fun mushrooms() = apply { this.mushrooms = true }
fun build(): Pizza {
require(size in listOf("Small", "Medium", "Large")) { "Invalid size" }
return Pizza(size, cheese, pepperoni, mushrooms)
}
}
}
fun main() {
val pizza = Pizza.Builder()
.size("Large")
.cheese()
.pepperoni()
.build()
println("Pizza: ${pizza.size}, cheese=${pizza.cheese}, pepperoni=${pizza.pepperoni}")
}
Структурные паттерны (Structural)
4. Adapter (Адаптер)
Адаптер позволяет объектам с несовместимыми интерфейсами работать вместе. В Kotlin удобно использовать функции-расширения.
// Старый интерфейс
interface OldLogger {
fun logMessage(msg: String)
}
// Новый интерфейс
interface NewLogger {
fun info(tag: String, message: String)
}
// Адаптер через extension function
fun OldLogger.toNewLogger(): NewLogger {
return object : NewLogger {
override fun info(tag: String, message: String) {
this@toNewLogger.logMessage("[$tag] $message")
}
}
}
class ConsoleOldLogger : OldLogger {
override fun logMessage(msg: String) = println(msg)
}
fun main() {
val oldLogger = ConsoleOldLogger()
val newLogger = oldLogger.toNewLogger()
newLogger.info("APP", "Hello from Adapter!")
}
5. Decorator (Декоратор)
Динамически добавляет объекту новые обязанности. В Kotlin можно использовать делегирование через by.
interface Coffee {
fun cost(): Double
fun description(): String
}
class SimpleCoffee : Coffee {
override fun cost() = 2.0
override fun description() = "Simple coffee"
}
class MilkDecorator(private val coffee: Coffee) : Coffee by coffee {
override fun cost() = coffee.cost() + 0.5
override fun description() = coffee.description() + " + milk"
}
class SugarDecorator(private val coffee: Coffee) : Coffee by coffee {
override fun cost() = coffee.cost() + 0.3
override fun description() = coffee.description() + " + sugar"
}
fun main() {
val coffee = SugarDecorator(MilkDecorator(SimpleCoffee()))
println("${coffee.description()} — $${coffee.cost()}")
}
Поведенческие паттерны (Behavioral)
6. Observer (Наблюдатель)
Паттерн "издатель-подписчик". В Kotlin есть встроенная поддержка через MutableStateFlow из kotlinx.coroutines.
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
class NewsPublisher {
private val _news = MutableStateFlow("No news yet")
val news = _news.asStateFlow()
fun publish(news: String) {
_news.value = news
}
}
fun main() = runBlocking {
val publisher = NewsPublisher()
// Подписчик 1
val job1 = launch {
publisher.news.collect { value ->
println("Subscriber 1 received: $value")
}
}
// Подписчик 2
val job2 = launch {
publisher.news.collect { value ->
println("Subscriber 2 received: $value")
}
}
publisher.publish("Kotlin 2.0 released!")
delay(100)
publisher.publish("New coroutines feature")
delay(200)
job1.cancel()
job2.cancel()
}
7. Strategy (Стратегия)
Позволяет менять алгоритм на лету. В Kotlin стратегии удобно передавать как лямбды.