🎯 Kotlin

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. Паттерны — это инструменты, а не украшения.

Лучшие практики и советы профессионалов

  1. Начинай с простого решения. Не внедряй паттерн, пока не столкнёшься с реальной проблемой. Часто обычная функция или класс решают задачу лучше.
  2. Используй встроенные возможности Kotlin. object для Singleton, sealed class для Factory, лямбды для Observer — это идиоматично и безопасно.
  3. Документируй паттерны. Если используешь Singleton, напиши комментарий: «Это синглтон, потому что...». Другие разработчики скажут спасибо.
  4. Тестируй изоляцию. Singleton сложно тестировать, потому что он хранит состояние. Для тестов используй внедрение зависимостей (Dependency Injection).
  5. Не бойся рефакторинга. Если код стал слишком сложным — упрости. Паттерны можно добавлять и удалять по мере необходимости.
  6. Изучай исходники. Посмотри, как реализованы паттерны в стандартной библиотеке Kotlin (например, lazy — это Singleton, Sequence — это Factory).
💡 Совет от профи: Паттерны — это не цель, а средство. Хороший код — это код, который легко читать и менять. Если паттерн делает код сложнее — не используй его.

Вопросы для самопроверки

  1. Что такое паттерн проектирования?
    Ответ: Это проверенное решение типовой проблемы в программировании. Не готовый код, а шаблон, который можно адаптировать.
  2. Для чего нужен Singleton?
    Ответ: Чтобы гарантировать, что у класса есть только один экземпляр, и предоставить глобальную точку доступа к нему.
  3. Как в Kotlin объявить Singleton?
    Ответ: С помощью ключевого слова object. Например: object MySingleton { ... }.
  4. В чём разница между Factory Method и обычным конструктором?
    Ответ: Factory Method решает, какой конкретный класс создать, и возвращает объект через общий интерфейс. Конструктор всегда создаёт объект одного конкретного класса.
  5. Какой тип класса в Kotlin лучше всего подходит для Factory Method?
    Ответ: sealed class — он позволяет ограничить набор возможных типов и обработать их в when.
  6. Какая главная проблема Observer?
    Ответ: Утечка памяти, если подписчики не отписываются от издателя.
  7. Можно ли использовать Singleton в многопоточном приложении?
    Ответ: Да, но нужно обеспечить потокобезопасность. В Kotlin object потокобезопасен, а вручную написанный Singleton требует синхронизации.
  8. Какой паттерн выбрать, если нужно создавать объекты разных типов, но код не должен знать о конкретных классах?
    Ответ: Factory Method.
  9. Что будет, если в Observer подписать один и тот же объект дважды?
    Ответ: Он получит два уведомления. Нужно проверять, есть ли уже подписчик в списке, перед добавлением.
  10. Нужно ли использовать паттерны в каждом проекте?
    Ответ: Нет. Паттерны применяются только тогда, когда есть соответствующая проблема. Излишнее использование усложняет код.

Итог и что изучить дальше

Мы разобрали три фундаментальных паттерна проектирования на языке Kotlin:

  • Singleton — для ситуаций, когда нужен ровно один экземпляр класса.
  • Factory Method — для гибкого создания объектов без привязки к конкретным классам.
  • Observer — для оповещения множества объектов об изменениях.

Ты узнал, как они работают «под капотом», увидел примеры от простых до сложных, и теперь можешь применять их в своих проектах. Помни: паттерны — это инструменты, а не догмы. Используй их с умом.

Что изучить дальше:

  • Другие паттерны: Builder, Adapter, Decorator, Strategy, Command. Они расширят твой арсенал.
  • Внедрение зависимостей (Dependency Injection): Как сделать код ещё более гибким и тестируемым.
  • Архитектурные паттерны: MVC, MVP, MVVM — для построения целых приложений.
  • Kotlin Coroutines: Как паттерны работают в асинхронном мире.
  • Рефакторинг: Как превратить плохой код в хороший, используя паттерны.
🎯 Главное: Пиши код, который понятен тебе через месяц. Паттерны — твои помощники в этом. Удачи в программировании!