Паттерны проектирования в Kotlin: когда и зачем
Вы когда-нибудь писали код, который спустя месяц становится загадкой даже для вас? Или сталкивались с ситуацией, когда небольшое изменение в одном месте ломает всё приложение? Добро пожаловать в реальный мир разработки! Именно здесь на помощь приходят паттерны проектирования — проверенные временем шаблоны решений типовых задач. Но в Kotlin, с его современным синтаксисом и мощными фичами, многие классические паттерны обретают второе дыхание. Давайте разберёмся, как и когда их применять, чтобы ваш код был не только рабочим, но и элегантным.
Почему Kotlin меняет правила игры?
Kotlin — это не просто «Java без бойлерплейта». Это язык, который впитал лучшее из функционального и объектно-ориентированного программирования. Благодаря функциям высшего порядка, лямбдам, data-классам и делегатам, многие паттерны становятся либо тривиальными, либо реализуются совершенно иначе. Например, паттерн Singleton в Java требует целую церемонию с volatile и synchronized. В Kotlin? Просто слово object. Но дьявол, как всегда, в деталях: важно не просто знать синтаксис, а понимать когда нужен паттерн, а когда он — излишняя архитектурная тяжесть.
Топ-4 паттерна, которые нужно знать каждому Kotlin-разработчику
Мы не будем перечислять все 23 паттерна GoF — многие из них в Kotlin решаются встроенными средствами. Вместо этого сосредоточимся на тех, где Kotlin даёт реальные преимущества.
1. Builder: когда конструктор — это боль
Представьте, что вы создаёте объект с 15 параметрами, половина из которых — опциональные. Классический конструктор превращается в кошмар, а телескопические конструкторы (один с 3 параметрами, другой с 5...) — в антипаттерн. Здесь на сцену выходит Builder.
В Java вы бы писали отдельный класс со сложной цепочкой методов. В Kotlin всё проще: используйте именованные аргументы и значения по умолчанию. Но если вам нужна именно кастомная логика валидации или построения — добро пожаловать в мир apply и scope-функций.
// Пример: Строим HTTP-запрос с помощью паттерна Builder
class HttpRequest private constructor(
val url: String,
val method: String,
val headers: Map<String, String>,
val body: String?
) {
// Вложенный Builder (DSL-стиль)
class Builder {
private var url: String = ""
private var method: String = "GET"
private var headers: MutableMap<String, String> = mutableMapOf()
private var body: String? = null
fun url(url: String) = apply { this.url = url }
fun method(method: String) = apply { this.method = method }
fun header(key: String, value: String) = apply { this.headers[key] = value }
fun body(body: String) = apply { this.body = body }
fun build(): HttpRequest {
require(url.isNotBlank()) { "URL не может быть пустым" }
return HttpRequest(url, method, headers.toMap(), body)
}
}
}
// Использование — лаконично и читаемо
val request = HttpRequest.Builder()
.url("https://api.example.com/data")
.method("POST")
.header("Content-Type", "application/json")
.body("""{"key": "value"}""")
.build()
println(request)
Преимущества: вы получаете гибкость без громоздкого кода. Паттерн Builder в Kotlin — это не про «написать много кода», а про контроль над созданием сложного объекта.
2. Singleton: один на всех
Singleton нужен, когда в приложении должен быть ровно один экземпляр класса: логгер, конфигурация, пул соединений. В Java это вечная головная боль с двойной проверкой блокировки. В Kotlin — просто объявите object.
// Ленивая инициализация и потокобезопасность — из коробки
object AppConfig {
val apiKey: String = loadApiKey()
val baseUrl: String = "https://api.example.com"
private fun loadApiKey(): String {
// Чтение из файла или переменных окружения
return System.getenv("API_KEY") ?: "default_key"
}
}
// Использование
fun main() {
println(AppConfig.apiKey) // Доступен глобально, но лениво инициализируется
}
Но будьте осторожны: злоупотребление Singleton — это антипаттерн. Он усложняет тестирование и скрывает зависимости. Используйте его только для действительно глобальных сущностей (конфигурация, системные логгеры). Для всего остального — внедрение зависимостей (Dependency Injection).
3. Strategy: алгоритмы на лету
Паттерн Strategy позволяет менять поведение объекта во время выполнения. Например, у вас есть калькулятор стоимости доставки, и вы хотите легко переключаться между «Обычной доставкой», «Экспрессом» и «Международной». В Kotlin это становится особенно элегантным благодаря лямбдам и функциям как объектам первого класса.
// Определяем стратегию как функциональный интерфейс
interface ShippingStrategy {
fun calculateCost(weight: Double, distance: Double): Double
}
// Конкретные стратегии
class StandardShipping : ShippingStrategy {
override fun calculateCost(weight: Double, distance: Double): Double {
return weight * 0.5 + distance * 0.1
}
}
class ExpressShipping : ShippingStrategy {
override fun calculateCost(weight: Double, distance: Double): Double {
return weight * 1.2 + distance * 0.5 + 10.0 // Надбавка за срочность
}
}
// Контекст, использующий стратегию
class ShippingCalculator(private var strategy: ShippingStrategy) {
fun setStrategy(strategy: ShippingStrategy) {
this.strategy = strategy
}
fun calculate(weight: Double, distance: Double): Double {
return strategy.calculateCost(weight, distance)
}
}
// Использование
fun main() {
val calculator = ShippingCalculator(StandardShipping())
println("Стандартная доставка: ${calculator.calculate(5.0, 100.0)}")
calculator.setStrategy(ExpressShipping())
println("Экспресс-доставка: ${calculator.calculate(5.0, 100.0)}")
}
А теперь смотрите, как Kotlin упрощает жизнь: вместо создания отдельного класса для каждой стратегии, можно использовать лямбду:
// Kotlin-версия: стратегия как лямбда
class ShippingCalculatorLambda(private var strategy: (Double, Double) -> Double) {
fun setStrategy(strategy: (Double, Double) -> Double) {
this.strategy = strategy
}
fun calculate(weight: Double, distance: Double): Double {
return strategy(weight, distance)
}
}
fun main() {
val calc = ShippingCalculatorLambda { w, d -> w * 0.5 + d * 0.1 }
println("Лямбда-доставка: ${calc.calculate(5.0, 100.0)}")
// Меняем стратегию на лету
calc.setStrategy { w,