Entwurfsmuster in Kotlin: Eine ausführliche Anleitung
Что это такое и зачем? (аналогия из реальной жизни)
Представь, что ты строишь дом. Можно просто накидать кирпичи как попало — и получится кривая лачуга. А можно использовать проверенные чертежи: фундамент, несущие стены, перекрытия, крышу. Эти чертежи — и есть паттерны проектирования (design patterns). Только в программировании.
Паттерн — это не готовый код, а проверенное решение типовой проблемы. Как рецепт пирога: ты можешь менять начинку, но основа (тесто, пропорции) остаётся неизменной. Паттерны помогают:
- Не изобретать велосипед каждый раз
- Делать код понятным для других программистов
- Легко менять программу, не ломая её
- Избегать типичных ошибок
💡 Важно: Паттерны — это не магия. Это инструменты. Используй их, когда они реально нужны, а не «чтобы было красиво».
В этой статье мы разберём три самых популярных паттерна на языке Kotlin: Singleton (одиночка), Factory Method (фабричный метод) и Observer (наблюдатель). Ты узнаешь, как они работают «под капотом», увидишь живые примеры и научишься применять их в своих проектах.
Теория: как это работает изнутри
1. Singleton (Одиночка)
Проблема: В программе должен быть ровно один экземпляр класса (например, подключение к базе данных, настройки приложения, логгер). Если создать два — будет путаница и лишняя трата памяти.
Решение: Класс сам контролирует своё создание. Он хранит единственный экземпляр и выдаёт его по запросу. Конструктор скрыт — никто не сможет сделать MyClass() напрямую.
2. Factory Method (Фабричный метод)
Проблема: Нужно создавать объекты разных типов, но код не должен знать, какой именно тип понадобится заранее. Например, игра: враги бывают разных видов (орки, эльфы, драконы). Если в каждом месте писать Orc() или Dragon(), то при добавлении нового врага придётся менять весь код.
Решение: Создаём «фабрику» — метод, который решает, какой объект создать, и возвращает его. Код, который использует фабрику, ничего не знает о конкретных классах — он работает через общий интерфейс.
3. Observer (Наблюдатель)
Проблема: Один объект (издатель) меняет своё состояние, и нужно оповестить об этом много других объектов (подписчиков). Например, погодная станция измеряет температуру, а несколько экранов показывают её. Если каждый экран будет сам опрашивать станцию каждую секунду — это неэффективно.
Решение: Издатель хранит список подписчиков. Когда состояние меняется, он просто рассылает всем сообщение. Подписчики сами решают, что делать с новыми данными.
📌 Запомни: Singleton — «один на всех», Factory — «создай то, не знаю что», Observer — «я изменился — всем привет».
Синтаксис в Kotlin: все формы и варианты
Kotlin — современный язык, и многие паттерны в нём реализуются очень лаконично. Давай посмотрим, как выглядят наши три паттерна.
Singleton в Kotlin
В Kotlin есть специальное ключевое слово object. Оно создаёт класс-одиночку автоматически:
// Объявляем синглтон
object DatabaseConfig {
val url = "jdbc:postgresql://localhost:5432/mydb"
val user = "admin"
val password = "secret"
fun connect() {
println("Подключаемся к $url")
}
}
// Использование — без создания экземпляра!
fun main() {
DatabaseConfig.connect() // Выведет: Подключаемся к jdbc:postgresql://localhost:5432/mydb
println(DatabaseConfig.url) // jdbc:postgresql://localhost:5432/mydb
}
Если нужно больше контроля (например, ленивая инициализация или передача параметров), используем классический вариант с companion object:
class Logger private constructor(val name: String) {
companion object {
private var instance: Logger? = null
fun getInstance(name: String): Logger {
if (instance == null) {
instance = Logger(name)
}
return instance!!
}
}
fun log(message: String) {
println("[$name] $message")
}
}
fun main() {
val logger = Logger.getInstance("MyApp")
logger.log("Старт программы")
}
Factory Method в Kotlin
Чаще всего реализуется через sealed class (запечатанный класс) и companion object:
// Запечатанный класс — все возможные типы известны
sealed class Enemy {
class Orc(val health: Int) : Enemy()
class Dragon(val firePower: Int) : Enemy()
class Elf(val agility: Int) : Enemy()
companion object {
// Фабричный метод
fun create(type: String, power: Int): Enemy {
return when (type.lowercase()) {
"orc" -> Orc(power * 10)
"dragon" -> Dragon(power * 50)
"elf" -> Elf(power * 8)
else -> throw IllegalArgumentException("Неизвестный тип врага: $type")
}
}
}
}
fun main() {
val enemy1 = Enemy.create("orc", 5) // Orc(health=50)
val enemy2 = Enemy.create("dragon", 2) // Dragon(firePower=100)
println(enemy1) // Orc(health=50)
}
Observer в Kotlin
В Kotlin есть встроенная поддержка через делегированные свойства и лямбды. Но для понимания сделаем классическую реализацию:
// Интерфейс наблюдателя
interface Observer {
fun update(data: String)
}
// Издатель
class WeatherStation {
private val observers = mutableListOf()
private var temperature: String = ""
fun subscribe(observer: Observer) {
observers.add(observer)
}
fun unsubscribe(observer: Observer) {
observers.remove(observer)
}
fun setTemperature(newTemp: String) {
temperature = newTemp
notifyObservers()
}
private fun notifyObservers() {
observers.forEach { it.update(temperature) }
}
}
// Конкретный наблюдатель — экран
class DisplayScreen(val name: String) : Observer {
override fun update(data: String) {
println("$name: Температура изменилась на $data")
}
}
fun main() {
val station = WeatherStation()
val screen1 = DisplayScreen("Экран №1")
val screen2 = DisplayScreen("Экран №2")
station.subscribe(screen1)
station.subscribe(screen2)
station.setTemperature("25°C")
// Вывод:
// Экран №1: Температура изменилась на 25°C
// Экран №2: Температура изменилась на 25°C
}
Примеры кода — от простого к сложному
Пример 1: Простой Singleton — счётчик посещений
// Синглтон для подсчёта посещений сайта
object VisitCounter {
private var count = 0
fun visit() {
count++
println("Посещение №$count")
}
fun getCount(): Int = count
}
fun main() {
VisitCounter.visit() // Посещение №1
VisitCounter.visit() // Посещение №2
println("Всего: ${VisitCounter.getCount()}") // Всего: 2
}
Пример 2: Singleton с ленивой инициализацией
class HeavyResource private constructor() {
companion object {
private var instance: HeavyResource? = null
fun getInstance(): HeavyResource {
if (instance == null) {
println("Создаём тяжёлый ресурс...")
instance = HeavyResource()
}
return instance!!
}
}
fun doWork() {
println("Работаем с ресурсом")
}
}
fun main() {
// Ресурс создастся только при первом вызове
HeavyResource.getInstance().doWork()
HeavyResource.getInstance().doWork() // Повторно не создаётся
}
Пример 3: Factory Method — создание геометрических фигур
sealed class Shape {
data class Circle(val radius: Double) : Shape()
data class Rectangle(val width: Double, val height: Double) : Shape()
data class Triangle(val sideA: Double, val sideB: Double, val sideC: Double) : Shape()
companion object {
fun create(type: String, vararg params: Double): Shape {
return when (type.lowercase()) {
"circle" -> Circle(params[0])
"rectangle" -> Rectangle(params[0], params[1])
"triangle" -> Triangle(params[0], params[1], params[2])
else -> throw IllegalArgumentException("Неизвестная фигура")
}
}
}
}
fun main() {
val circle = Shape.create("circle", 5.0)
val rect = Shape.create("rectangle", 3.0, 4.0)
println(circle) // Circle(radius=5.0)
println(rect) // Rectangle(width=3.0, height=4.0)
}
Пример 4: Factory с интерфейсом (более гибкий)
interface Payment {
fun pay(amount: Double)
}
class CreditCard : Payment {
override fun pay(amount: Double) {
println("Оплата картой: $amount руб.")
}
}
class PayPal : Payment {
override fun pay(amount: Double) {
println("Оплата через PayPal: $amount руб.")
}
}
class Cash : Payment {
override fun pay(amount: Double) {
println("Оплата наличными: $amount руб.")
}
}
object PaymentFactory {
fun create(type: String): Payment {
return when (type.lowercase()) {
"card" -> CreditCard()
"paypal" -> PayPal()
"cash" -> Cash()
else -> throw IllegalArgumentException("Неизвестный тип оплаты")
}
}
}
fun main() {
val payment = PaymentFactory.create("card")
payment.pay(1000.0) // Оплата картой: 1000.0 руб.
}
Пример 5: Observer — система уведомлений
interface Subscriber {
fun notify(message: String)
}
class EmailSubscriber(val email: String) : Subscriber {
override fun notify(message: String) {
println("Отправляем email на $email: $message")
}
}
class SMSSubscriber(val phone: String) : Subscriber {
override fun notify(message: String) {
println("Отправляем SMS на $phone: $message")
}
}
class NewsPublisher {
private val subscribers = mutableListOf()
fun subscribe(sub: Subscriber) {
subscribers.add(sub)
}
fun unsubscribe(sub: Subscriber) {
subscribers.remove(sub)
}
fun publishNews(news: String) {
println("Новость: $news")
subscribers.forEach { it.notify(news) }
}
}
fun main() {
val publisher = NewsPublisher()
val user1 = EmailSubscriber("user1@example.com")
val user2 = SMSSubscriber("+7-999-123-45-67")
publisher.subscribe(user1)
publisher.subscribe(user2)
publisher.publishNews("Вышла новая версия Kotlin 2.0!")
// Вывод:
// Новость: Вышла новая версия Kotlin 2.0!
// Отправляем email на user1@example.com: Вышла новая версия Kotlin 2.0!
// Отправляем SMS на +7-999-123-45-67: Вышла новая версия Kotlin 2.0!
}
Пример 6: Observer с лямбдами (Kotlin-стиль)
class EventBus {
private val listeners = mutableListOf<(String) -> Unit>()
fun onEvent(listener: (String) -> Unit) {
listeners.add(listener)
}
fun emit(event: String) {
listeners.forEach { it(event) }
}
}
fun main() {
val bus = EventBus()
// Подписываемся с помощью лямбды
bus.onEvent { event -> println("Обработчик 1: $event") }
bus.onEvent { event -> println("Обработчик 2: $event") }
bus.emit("Событие произошло!")
// Вывод:
// Обработчик 1: Событие произошло!
// Обработчик 2: Событие произошло!
}
Пример 7: Комбинируем Factory + Singleton
// Синглтон-фабрика для создания логгеров
object LoggerFactory {
private val loggers = mutableMapOf()
fun getLogger(name: String): Logger {
return loggers.getOrPut(name) {
println("Создаём новый логгер: $name")
Logger(name)
}
}
}
class Logger(private val name: String) {
fun log(message: String) {
println("[$name] $message")
}
}
fun main() {
val logger1 = LoggerFactory.getLogger("System")
val logger2 = LoggerFactory.getLogger("System") // Вернёт тот же экземпляр
val logger3 = LoggerFactory.getLogger("Network")
logger1.log("Старт системы")
logger3.log("Подключение к серверу")
// Вывод:
// Создаём новый логгер: System
// Создаём новый логгер: Network
// [System] Старт системы
// [Network] Подключение к серверу
}
Сравнение с Python: чем похоже и чем отличается
| Аспект | Kotlin | Python |
|---|---|---|
| Singleton | Встроенная поддержка через object. Проще некуда. |
Нет встроенного механизма. Используют __new__ или декораторы. |
| Factory Method | Часто через sealed class + companion object. Строгая типизация. |
Через обычные функции или классы. Динамическая типизация — проще, но можно ошибиться. |
| Observer | Через интерфейсы или лямбды. Нужно писать вручную. | Есть встроенный модуль observable в PyQt, но в чистом Python — тоже вручную. |
| Типизация | Статическая. Компилятор проверяет типы на этапе компиляции. | Динамическая. Ошибки типов проявляются только во время выполнения. |
| Производительность | Выше (компилируется в байт-код JVM). | Ниже (интерпретируемый язык). |
| Сложность для новичка | Средняя. Нужно понимать типы, sealed class, object. | Низкая. Можно начать писать паттерны сразу. |
🔍 Вывод: Kotlin даёт больше гарантий безопасности на этапе компиляции, но требует больше знаний. Python проще для экспериментов, но легче допустить ошибку в большом проекте.
Типичные ошибки и подводные камни
Ошибка 1: Singleton — забыли про многопоточность
Если два потока одновременно вызовут getInstance(), могут создать два экземпляра. В Kotlin object потокобезопасен, но вручную написанный Singleton — нет.
// НЕПРАВИЛЬНО (не потокобезопасно)
class BadSingleton private constructor() {
companion object {
private var instance: BadSingleton? = null
fun getInstance(): BadSingleton {
if (instance == null) { // Два потока могут войти сюда одновременно
instance = BadSingleton()
}
return instance!!
}
}
}
// ПРАВИЛЬНО (с синхронизацией)
class GoodSingleton private constructor() {
companion object {
@Volatile
private var instance: GoodSingleton? = null
fun getInstance(): GoodSingleton {
return instance ?: synchronized(this) {
instance ?: GoodSingleton().also { instance = it }
}
}
}
}
Ошибка 2: Factory — слишком много условий
Если в фабрике 20 разных типов, when станет огромным. Лучше разбить на несколько фабрик или использовать рефлексию.
Ошибка 3: Observer — утечка памяти
Если подписчик не отписался, он будет висеть в памяти вечно. Всегда отписывайтесь, когда объект больше не нужен.
// Пример утечки
class LeakyActivity {
private val publisher = NewsPublisher()
fun start() {
publisher.subscribe(object : Subscriber {
override fun notify(message: String) {
println(message)
}
})
// Никто не вызвал unsubscribe() — объект остаётся в памяти!
}
}
Ошибка 4: Использование паттерна «на всякий случай»
Не нужно делать Singleton для каждого класса. Если экземпляров может быть несколько — не используй Singleton. Паттерны — это инструменты, а не украшения.
Лучшие практики и советы профессионалов
- Начинай с простого решения. Не внедряй паттерн, пока не столкнёшься с реальной проблемой. Часто обычная функция или класс решают задачу лучше.
- Используй встроенные возможности Kotlin.
objectдля Singleton,sealed classдля Factory, лямбды для Observer — это идиоматично и безопасно. - Документируй паттерны. Если используешь Singleton, напиши комментарий: «Это синглтон, потому что...». Другие разработчики скажут спасибо.
- Тестируй изоляцию. Singleton сложно тестировать, потому что он хранит состояние. Для тестов используй внедрение зависимостей (Dependency Injection).
- Не бойся рефакторинга. Если код стал слишком сложным — упрости. Паттерны можно добавлять и удалять по мере необходимости.
- Изучай исходники. Посмотри, как реализованы паттерны в стандартной библиотеке Kotlin (например,
lazy— это Singleton,Sequence— это Factory).
💡 Совет от профи: Паттерны — это не цель, а средство. Хороший код — это код, который легко читать и менять. Если паттерн делает код сложнее — не используй его.
Вопросы для самопроверки
- Что такое паттерн проектирования?
Ответ: Это проверенное решение типовой проблемы в программировании. Не готовый код, а шаблон, который можно адаптировать. - Для чего нужен Singleton?
Ответ: Чтобы гарантировать, что у класса есть только один экземпляр, и предоставить глобальную точку доступа к нему. - Как в Kotlin объявить Singleton?
Ответ: С помощью ключевого словаobject. Например:object MySingleton { ... }. - В чём разница между Factory Method и обычным конструктором?
Ответ: Factory Method решает, какой конкретный класс создать, и возвращает объект через общий интерфейс. Конструктор всегда создаёт объект одного конкретного класса. - Какой тип класса в Kotlin лучше всего подходит для Factory Method?
Ответ:sealed class— он позволяет ограничить набор возможных типов и обработать их вwhen. - Какая главная проблема Observer?
Ответ: Утечка памяти, если подписчики не отписываются от издателя. - Можно ли использовать Singleton в многопоточном приложении?
Ответ: Да, но нужно обеспечить потокобезопасность. В Kotlinobjectпотокобезопасен, а вручную написанный Singleton требует синхронизации. - Какой паттерн выбрать, если нужно создавать объекты разных типов, но код не должен знать о конкретных классах?
Ответ: Factory Method. - Что будет, если в Observer подписать один и тот же объект дважды?
Ответ: Он получит два уведомления. Нужно проверять, есть ли уже подписчик в списке, перед добавлением. - Нужно ли использовать паттерны в каждом проекте?
Ответ: Нет. Паттерны применяются только тогда, когда есть соответствующая проблема. Излишнее использование усложняет код.
Итог и что изучить дальше
Мы разобрали три фундаментальных паттерна проектирования на языке Kotlin:
- Singleton — для ситуаций, когда нужен ровно один экземпляр класса.
- Factory Method — для гибкого создания объектов без привязки к конкретным классам.
- Observer — для оповещения множества объектов об изменениях.
Ты узнал, как они работают «под капотом», увидел примеры от простых до сложных, и теперь можешь применять их в своих проектах. Помни: паттерны — это инструменты, а не догмы. Используй их с умом.
Что изучить дальше:
- Другие паттерны: Builder, Adapter, Decorator, Strategy, Command. Они расширят твой арсенал.
- Внедрение зависимостей (Dependency Injection): Как сделать код ещё более гибким и тестируемым.
- Архитектурные паттерны: MVC, MVP, MVVM — для построения целых приложений.
- Kotlin Coroutines: Как паттерны работают в асинхронном мире.
- Рефакторинг: Как превратить плохой код в хороший, используя паттерны.
🎯 Главное: Пиши код, который понятен тебе через месяц. Паттерны — твои помощники в этом. Удачи в программировании!