Паттерны проектирования в Kotlin

от автора

Паттерны проектирования в Kotlin

Говорят, что «паттерны проектирования — это обходные пути недостатков определенного языка программирования». Самое забавное, что это сказали сторонники Lisp и Scheme, у которых в языках всё было в порядке.

Но, похоже, разработчики языка 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». Вам она понравится, даже если вы не планируете использовать этот язык в ближайшем будущем (хотя нет причин этого не делать).


ссылка на оригинал статьи https://habr.com/post/421873/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *