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