Паттерны проектирования в 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.


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

Данные о правообладателе фото и видеоматериалов взяты с сайта «Хабрахабр», подробнее в Условиях использования
Анализ
×