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

Перевод статьи «Kotlin Design Patterns»

0 комментариев к статье "Паттерны проектирования в Kotlin"

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