Паттерны проектирования в Kotlin с примерами: от А до Я
Паттерны проектирования (design patterns) — это проверенные решения типовых задач разработки. В Kotlin, благодаря его лаконичному синтаксису и мощным возможностям (data class, object, extension functions, корутины), многие классические паттерны реализуются проще и элегантнее, чем в Java. В этой статье мы разберем ключевые паттерны на Kotlin от порождающих до поведенческих, с реальными примерами кода.
1. Порождающие паттерны (Creational)
Эти паттерны управляют процессом создания объектов, делая систему независимой от способа их создания.
1.1. Singleton (Одиночка)
Гарантирует, что у класса есть только один экземпляр. В Kotlin для этого используется ключевое слово object.
object DatabaseConnection {
val connectionString: String = "jdbc:postgresql://localhost:5432/mydb"
fun connect() {
println("Connecting to $connectionString")
}
}
fun main() {
DatabaseConnection.connect()
println(DatabaseConnection.connectionString)
}
Когда использовать: для менеджеров конфигурации, пулов потоков, кэшей.
1.2. Factory Method (Фабричный метод)
Определяет интерфейс для создания объекта, но позволяет подклассам изменять тип создаваемого объекта.
// Абстрактный продукт
interface Notification {
fun send(message: String)
}
// Конкретные продукты
class EmailNotification : Notification {
override fun send(message: String) = println("Email: $message")
}
class SMSNotification : Notification {
override fun send(message: String) = println("SMS: $message")
}
// Фабрика
abstract class NotificationFactory {
abstract fun createNotification(): Notification
fun notifyUser(message: String) {
val notification = createNotification()
notification.send(message)
}
}
class EmailFactory : NotificationFactory() {
override fun createNotification(): Notification = EmailNotification()
}
class SMSFactory : NotificationFactory() {
override fun createNotification(): Notification = SMSNotification()
}
fun main() {
val factory: NotificationFactory = EmailFactory()
factory.notifyUser("Hello from Kotlin!")
}
Когда использовать: когда заранее неизвестен тип создаваемых объектов.
1.3. Builder (Строитель)
Позволяет создавать сложные объекты шаг за шагом. В Kotlin отлично реализуется через apply и именованные аргументы.
data class Pizza(
val size: String,
val cheese: Boolean = false,
val pepperoni: Boolean = false,
val mushrooms: Boolean = false
) {
// Встроенный Builder
class Builder(private val size: String) {
private var cheese: Boolean = false
private var pepperoni: Boolean = false
private var mushrooms: Boolean = false
fun cheese(value: Boolean) = apply { cheese = value }
fun pepperoni(value: Boolean) = apply { pepperoni = value }
fun mushrooms(value: Boolean) = apply { mushrooms = value }
fun build() = Pizza(size, cheese, pepperoni, mushrooms)
}
}
fun main() {
val pizza = Pizza.Builder("Large")
.cheese(true)
.pepperoni(true)
.build()
println(pizza)
}
Когда использовать: при большом количестве опциональных параметров.
2. Структурные паттерны (Structural)
Эти паттерны определяют способы построения классов и объектов в более крупные структуры.
2.1. Adapter (Адаптер)
Позволяет объектам с несовместимыми интерфейсами работать вместе. В Kotlin часто используется через extension functions.
// Старый интерфейс
interface OldLogger {
fun logMessage(msg: String)
}
// Новый интерфейс
interface NewLogger {
fun log(level: String, message: String)
}
// Адаптер через extension function
fun OldLogger.adaptToNew(): NewLogger {
return object : NewLogger {
override fun log(level: String, message: String) {
this@adaptToNew.logMessage("[$level] $message")
}
}
}
fun main() {
val oldLogger = object : OldLogger {
override fun logMessage(msg: String) = println(msg)
}
val newLogger = oldLogger.adaptToNew()
newLogger.log("INFO", "System started")
}
Когда использовать: при интеграции со сторонними библиотеками или legacy-кодом.
2.2. Decorator (Декоратор)
Динамически добавляет объекту новые обязанности. В Kotlin отлично реализуется через делегирование.
interface Coffee {
val cost: Double
val description: String
}
class SimpleCoffee : Coffee {
override val cost: Double = 5.0
override val description: String = "Black coffee"
}
class MilkDecorator(private val coffee: Coffee) : Coffee {
override val cost: Double = coffee.cost + 1.5
override val description: String = coffee.description + ", milk"
}
class SugarDecorator(private val coffee: Coffee) : Coffee {
override val cost: Double = coffee.cost + 0.5
override val description: String = coffee.description + ", sugar"
}
fun main() {
val coffee = MilkDecorator(SugarDecorator(SimpleCoffee()))
println("${coffee.description}: \$${coffee.cost}")
}
Когда использовать: когда нужно добавлять функциональность динамически, избегая наследования.
3. Поведенческие паттерны (Behavioral)
Эти паттерны определяют алгоритмы и распределение обязанностей между объектами.
3.1. Observer (Наблюдатель)
Определяет зависимость "один ко многим" между объектами. В Kotlin можно использовать Delegates.observable.
import kotlin.properties.Delegates
class WeatherStation {
var temperature: Int by Delegates.observable(20) { _, old, new ->
println("Temperature changed from $old to $new")
}
}
fun main() {
val station = WeatherStation()
station.temperature = 25 // вызовет лямбду
station.temperature = 30
}
Для сложных сценариев используйте MutableLiveData или Flow из корутин.
3.2. Strategy (Стратегия)
Определяет семейство алгоритмов и делает их взаимозаменяемыми.
interface PaymentStrategy {
fun pay(amount: Double)
}
class CreditCardPayment(private val cardNumber: String) : PaymentStrategy {
override fun pay(amount: Double) {
println("Paid $$amount via credit card $cardNumber")
}
}
class PayPalPayment(private val email: String) : PaymentStrategy {
override fun pay(amount: Double) {
println("Paid $$amount via PayPal ($email)")
}
}
class ShoppingCart {
private var strategy: PaymentStrategy? = null
fun setPaymentStrategy(strategy: PaymentStrategy) {
this.strategy