Design Patterns in Kotlin

Original author: Alexey Soshin
  • Transfer

Design Patterns in Kotlin


They say that "design patterns are workarounds for the shortcomings of a particular programming language." An interesting proposition, if only it was not told by the Lisp and Schema apologists .


But it seems that the developers of the Kotlin language have taken this statement really to heart.


Singleton


Of course, the first pattern that comes to mind is the Loner. And it is built right into the language in the form of the object keyword :


objectJustSingleton{
    val value : String = "Just a value"
}

Now the field JustSingleton.valuewill be available from anywhere in the package.


And no, this is not a static initialization, as it may seem. Let's try to initialize this field with some delay inside:


object SlowSingleton {
    val value : String
    init {
        var uuid = ""
        val total = measureTimeMillis {
            println("Computing")
            for (i in1..10_000_000) {
                uuid = UUID.randomUUID().toString()
            }
        }
        value = uuid
        println("Done computing in ${total}ms")
    }
}

Lazy initialization occurs on the first call:


@org.testng.annotations.Test
funtestSingleton() {
    println("Test started")
    for (i in1..3) {
        val total = measureTimeMillis {
                println(SlowSingleton.value)
        }
        println("Took $total ms")
    }
}

At the output we get:


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

Note that if you do not use this object, the operation takes 0 ms, although the object is still defined in your code.


val total = measureTimeMillis {
  //println(SlowSingleton.value)
}

At the exit:


Test started
Took 0 ms
Took 0 ms
Took 0 ms

Decorator


Then comes the decorator . This is a pattern that allows you to add a bit of functionality on top of some other class. Yes, IntelliJ can create it for you. But Kotlin went even further.


How about every time you add a new key in the HashMap, we get a message about it?


In the constructor, you specify the instance to which you delegate all methods using the keyword by .


/**
 * Using `by` keyword you can delegate all but overridden methods
 */classHappyMap<K, V>(val map : MutableMap<K, V> = mutableMapOf()) : MutableMap<K, V> by map{
    overridefunput(key: K, value: V): V? {
        return map.put(key, value).apply {
            if (this == null) {
                println("Yay! $key")
            }
        }
    }
}

Note that we can access the elements of our map through square brackets and use all other methods in the same way as in the usual HashMap.


@org.testng.annotations.Test
funtestDecorator() {
    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))
}

Factory Method


Companion object makes it easy to implement the Factory method . This is the pattern with which the object controls its initialization process in order to hide some secrets within itself.


classSecretiveGirlprivateconstructor(val age: Int,
                                        val name: String = "A girl has no name",
                                        val desires: String = "A girl has no desires") {
    companionobject {
        funnewGirl(vararg desires : String) : SecretiveGirl {
            return SecretiveGirl(17, desires = desires.joinToString(", "))
        }
        funnewGirl(name : String) : SecretiveGirl {
            return SecretiveGirl(17, name = name)
        }
    }
}

Now no one can change the age of SecretiveGirl:


@org.testng.annotations.Test
funFactoryMethodTest() {
    // 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)
}

Strategy


The last one for today is Strategy . Since Kotlin has high-order functions , implementing this pattern is also very simple:


classUncertainAnimal{
    var makeSound = fun() {
        println("Meow!")
    }
}

And dynamically change the behavior:


@org.testng.annotations.Test
funtestStrategy() {
    val someAnimal = UncertainAnimal()
    val output = captureOutput {
        someAnimal.makeSound()
        someAnimal.makeSound = fun() {
            println("Woof!")
        }
        someAnimal.makeSound()
    }
    assertEquals(listOf("Meow!", "Woof!"), output)
}

Please note that this is indeed the Strategy pattern, and you cannot change the method signature (hello, JS!)


// Won't compile!
someAnimal.makeSound = fun(message : String) {
   println("$message")
}

All code is available on my GitHub page .


And if you are interested in learning more about Kotlin and the design patterns embedded in it, there is an excellent book “Kotlin in Action” . You will like it, even if you do not plan to use this language in the near future (although there is no reason not to).


Also popular now: