
Говорят, что «паттерны проектирования — это обходные пути недостатков определенного языка программирования». Интересное суждение, если бы только оно не было сказано апологетами Lisp и Schema.
Но, похоже, разработчики языка Kotlin восприняли это высказывание по-настоящему близко к сердцу.
Одиночка (Singleton)
Конечно, первый паттерн, который приходит на ум, — Одиночка. И он встроен прямо в язык в виде ключевого слова object:
object JustSingleton { val value : String = "Just a value" }
Теперь поле JustSingleton.value будет доступно из любого места в пакете.
И нет, это не статическая инициализация, как может показаться. Давайте попробуем инициализировать это поле с некоторой задержкой внутри:
object SlowSingleton { val value : String init { var uuid = "" val total = measureTimeMillis { println("Computing") for (i in 1..10_000_000) { uuid = UUID.randomUUID().toString() } } value = uuid println("Done computing in ${total}ms") } }
Происходит ленивая инициализация при первом вызове:
@org.testng.annotations.Test fun testSingleton() { println("Test started") for (i in 1..3) { val total = measureTimeMillis { println(SlowSingleton.value) } println("Took $total ms") } }
На выходе получаем:
Test started Computing Done computing in 5376ms "45f7d567-9b3e-4099-98e6-569ebc26ecdf" Took 5377 ms "45f7d567-9b3e-4099-98e6-569ebc26ecdf" Took 0 ms "45f7d567-9b3e-4099-98e6-569ebc26ecdf" Took 0 ms
Обратите внимание, если вы не используете этот объект, операция проходит за 0 мс, хотя объект всё ещё определён в вашем коде.
val total = measureTimeMillis { //println(SlowSingleton.value) }
На выходе:
Test started Took 0 ms Took 0 ms Took 0 ms
Декоратор
Затем идет Декоратор. Это паттерн, который позволяет добавить немного функциональности поверх какого-то другого класса. Да, IntelliJ может создать его за вас. Но Kotlin пошёл ещё дальше.
Как насчёт того, чтобы каждый раз при добавлении нового ключа в HashMap, мы получали сообщение об этом?
В конструкторе вы определяете экземпляр, которому делегируете все методы, используя ключевое слово by.
/** * Using `by` keyword you can delegate all but overridden methods */ class HappyMap<K, V>(val map : MutableMap<K, V> = mutableMapOf()) : MutableMap<K, V> by map{ override fun put(key: K, value: V): V? { return map.put(key, value).apply { if (this == null) { println("Yay! $key") } } } }
Заметьте, что мы можем получать доступ к элементам нашей мапы через квадратные скобки и использовать все остальные методы так же, как и в обычной HashMap.
@org.testng.annotations.Test fun testDecorator() { val map = HappyMap<String, String>() val result = captureOutput { map["A"] = "B" map["B"] = "C" map["A"] = "C" map.remove("A") map["A"] = "C" } assertEquals(mapOf("A" to "C", "B" to "C"), map.map) assertEquals(listOf("Yay! A", "Yay! B", "Yay! A"), (result)) }
Фабричный метод
Companion object позволяет легко реализовать Фабричный метод. Это тот паттерн, при помощи которого объект контролирует процесс своей инициализации для того, чтобы скрывать какие-то секреты внутри себя.
class SecretiveGirl private constructor(val age: Int, val name: String = "A girl has no name", val desires: String = "A girl has no desires") { companion object { fun newGirl(vararg desires : String) : SecretiveGirl { return SecretiveGirl(17, desires = desires.joinToString(", ")) } fun newGirl(name : String) : SecretiveGirl { return SecretiveGirl(17, name = name) } } }
Теперь никто не может изменить возраст SecretiveGirl:
@org.testng.annotations.Test fun FactoryMethodTest() { // Cannot do this, constructor is private // val arya = SecretiveGirl(); val arya1 = SecretiveGirl.newGirl("Arry") assertEquals(17, arya1.age) assertEquals("Arry", arya1.name) assertEquals("A girl has no desires", arya1.desires) val arya2 = SecretiveGirl.newGirl("Cersei Lannister", "Joffrey", "Ilyn Payne") assertEquals(17, arya2.age) assertEquals("A girl has no name", arya2.name) assertEquals("Cersei Lannister, Joffrey, Ilyn Payne", arya2.desires) }
Стратегия
Последний на сегодня — Стратегия. Поскольку в Kotlin есть функции высокого порядка, реализовать этот паттерн тоже очень просто:
class UncertainAnimal { var makeSound = fun () { println("Meow!") } }
И динамически менять поведение:
@org.testng.annotations.Test fun testStrategy() { val someAnimal = UncertainAnimal() val output = captureOutput { someAnimal.makeSound() someAnimal.makeSound = fun () { println("Woof!") } someAnimal.makeSound() } assertEquals(listOf("Meow!", "Woof!"), output) }
Обратите внимание, что это действительно паттерн Стратегия, и измененить сигнатуру метода нельзя (привет, JS!)
// Won't compile! someAnimal.makeSound = fun (message : String) { println("$message") }
Весь код доступен на моей странице GitHub.
И если вам интересно узнать больше о Kotlin и встроенных в него паттернах проектирования, есть отличная книга «Kotlin in Action». Вам она понравится, даже если вы не планируете использовать этот язык в ближайшем будущем (хотя нет причин этого не делать).
