Sunday, June 4, 2023

Observer Design Pattern with Kotlin

 Observer Design Patterns

Observer this design pattern will provide us with a bridge and dedicated to functional programming. So, what is Observer pattern about? You have one publisher, which may also be called a subject, that may have many subscribers, which may also be called observers. Each time something interesting happens with the publisher, it should update all of its subscribers. This may look a lot like the Mediator design pattern, but there's a twist. Subscribers should be able to register or unregister themselves at runtime. In the classical implementation, all subscribers/observers need to implement a certain interface in order for the publisher to be able to update them. But since Kotlin has higher-order functions, we can omit this part. The publisher will still have to provide means for observers to be able to subscribe and unsubscribe.

Animal Choir 

So, animals have decided to have a choir of their own. The cat was elected as the conductor of the choir (it didn't like to sing anyway). The problem is that animals escaped from the Java world, and don't have a common interface. Instead, each has a different method to make a sound: 

class Bat {

    fun screech() {

        println("Eeeeeee")

    }

}

 class Turkey {

    fun gobble() {

        println("Gob-gob")

    }

}

 class Dog {

    fun bark() {

        println("Woof")

    }

 fun howl() {

        println("Auuuu")

}

Luckily, the cat was elected not only because it was vocally challenged, but also because it was smart enough to follow this chapter until now. So it knows that in the Kotlin world, it can accept functions: 

class Cat {

    ...

    fun joinChoir(whatToCall: ()->Unit) {

        ...

    }

 fun leaveChoir(whatNotToCall: ()->Unit) {

        ...

    }

    ...

Previously, we've seen how to pass a new function as an argument, as well as passing a literal function. But how do we pass a reference to a member function? That's what member reference operator is for—::: 

val catTheConductor = Cat()

val bat = Bat()

val dog = Dog()

val turkey = Turkey()

catTheConductor.joinChoir(bat::screech)

catTheConductor.joinChoir(dog::howl)

catTheConductor.joinChoir(dog::bark)

catTheConductor.joinChoir(turkey::gobble) 

Now the cat needs to save all those subscribers somehow. Luckily, we can put them on a map. What would be the key? It could be the function itself: 

class Cat {

 private val participants = mutableMapOf<()->Unit, ()->Unit>()

 fun joinChoir(whatToCall: ()->Unit) {

        participants.put(whatToCall, whatToCall)

    }

    ...

If all those ()->Unit instances are making you dizzy, be sure to use typealias to give them more semantic meaning, such as subscriber. The bat decides to leave the choir. After all, no one is able to hear its beautiful singing anyway: 

class Cat {

    ...

    fun leaveChoir(whatNotToCall: ()->Unit) {

        participants.remove(whatNotToCall)

}

    ...

All Bat needs to do is to pass its subscriber function again: catTheConductor.leaveChoir(bat::screech) That's the reason we used the map in the first place. Now Cat can call all its choir members and tell them to sing. Well, produce sounds: typealias Times = Int

 class Cat {

    ...

    fun conduct(n: Times) {

        for (p in participants.values) {

            for (i in 1..n) {

                p()

            }

        }

    }

The rehearsal went well. But Cat feels very tired after doing all those loops. It would rather delegate the job to choir members. That's not a problem at all:

class Cat {

    private val participants = mutableMapOf<(Int)->Unit, (Int)->Unit>()

 fun joinChoir(whatToCall: (Int)->Unit) {

        ...

    }

 fun leaveChoir(whatNotToCall: (Int)->Unit) {

        ...

    }

 fun conduct(n: Times) {

        for (p in participants.values) {

            p(n)

        }

    }

Our subscribers all look like turkeys here: class Turkey {

    fun gobble(repeat: Times) {

        for (i in 1..repeat) {

            println("Gob-gob")

        }

    }

Actually, it is a bit of a problem. What if the Cat was to tell each animal what sound to make: high or low? We'll have to change all subscribers again, and the Cat too.

While designing your publisher, pass single data classes with many properties, instead of sets of data classes or other types. That way, you'll have to refactor your subscribers less, in case new properties are added: 

enum class SoundPitch {HIGH, LOW}

data class Message(val repeat: Times, val pitch: SoundPitch)

 class Bat {

    fun screech(message: Message) {

        for (i in 1..message.repeat) {

            println("${message.pitch} Eeeeeee")

        }

    }

Make sure that your messages are immutable. Otherwise, you may experience strange behavior! What if you have sets of different messages you're sending from the same publisher? Use smart casts: 

interface Message {

    val repeat: Times

    val pitch: SoundPitch 

}

 data class LowMessage(override val repeat: Times) : Message {

 override val pitch = SoundPitch.LOW

}

 data class HighMessage(override val repeat: Times) : Message {

    override val pitch = SoundPitch.HIGH

}

class Bat {

    fun screech(message: Message) {

        when (message) {

            is HighMessage -> {

                for (i in 1..message.repeat) {

                    println("${message.pitch} Eeeeeee")

                }

            }

            else -> println("Can't :(")

        }

    }

}

Code Example :

package designpatterns.observer


fun main(args: Array<String>) {


val catTheConductor = Cat()

val bat = Bat()
val dog = Dog()
val turkey = Turkey()

catTheConductor.joinChoir(bat::screech)
catTheConductor.joinChoir(dog::howl)
catTheConductor.joinChoir(dog::bark)
catTheConductor.joinChoir(turkey::gobble)

catTheConductor.conduct(LowMessage(2))
println()

catTheConductor.leaveChoir(bat::screech)
catTheConductor.conduct(HighMessage(1))
}

typealias Times = Int

enum class SoundPitch { HIGH, LOW }
interface Message {
val repeat: Times
val pitch: SoundPitch
}

data class LowMessage(override val repeat: Times) : Message {
override val pitch = SoundPitch.LOW
}

data class HighMessage(override val repeat: Times) : Message {
override val pitch = SoundPitch.HIGH
}

class Cat {
private val participants = mutableMapOf<(Message) -> Unit,
(Message) -> Unit>()

fun joinChoir(whatToCall: (Message) -> Unit) {
participants.put(whatToCall, whatToCall)
}

fun leaveChoir(whatNotToCall: (Message) -> Unit) {
participants.remove(whatNotToCall)
}

fun conduct(message: Message) {
for (p in participants.values) {
p(message)
}
}
}

class Bat {
fun screech(message: Message) {
when (message) {
is HighMessage -> {
for (i in 1..message.repeat) {
println("${message.pitch} Eeeeeee")
}
}

else -> println("Can't :(")
}
}
}

class Turkey {
fun gobble(message: Message) {
for (i in 1..message.repeat) {
println("${message.pitch} Gob-gob")
}
}
}

class Dog {
fun bark(message: Message) {
for (i in 1..message.repeat) {
println("${message.pitch} Woof")
}
}

fun howl(message: Message) {
for (i in 1..message.repeat) {
println("${message.pitch} Auuuu")
}
}
}

Ouput :

Can't :( LOW Auuuu LOW Auuuu LOW Woof LOW Woof LOW Gob-gob LOW Gob-gob HIGH Auuuu HIGH Woof HIGH Gob-gob

Summary 

That was a long chapter. But we've also learned a lot. We finished covering all classical design patterns, including eleven behavioral ones. In Kotlin, functions can be passed to other functions, returned from functions, and assigned to variables. That's what the "functions as first-class citizens" concept is all about. If your class is all about behavior, it often makes sense to replace it with a function. Iterator is yet another operator in the language. Sealed classes help in making when statements exhaustive. The run extension function allows for controlling what will be returned from it. A lambda with a receiver allows more clear syntax in your DSLs. Another keyword, lateinit, tells the compiler to relax a bit in its null safety checks. Use with care! And finally, we covered how to reference an existing method with ::. In the next blog, we'll move on from an object-oriented programming paradigm with its well-known design patterns to another paradigm—functional programming.


No comments: