Friday, May 5, 2023

Decorator Design Pattern in Kotlin comparative analysis with java

 Understanding Structural Patterns

 This chapter covers structural patterns in Kotlin. In general, structural patterns deal with relationships between objects. We'll discuss how to extend the functionality of our objects without producing complex class hierarchies and how to adapt to changes in the future or how to fix some of the decisions taken in the past, as well as how to reduce the memory footprint of our program. we will cover the following topics: 

Decorator 

Adapter

 Bridge 

Composite

 Facade 

Flyweight 

Proxy

Decorator 

we discussed the Prototype design pattern, which allowed for creating instances of classes with slightly (or not so slightly) different data. What if we would like to create a set of classes with slightly different behavior though? Well, since functions in Kotlin are first-class citizens (more on that in a bit), you could use the Prototype design pattern to achieve that. After all, that's what JavaScript does successfully. But the goal is to discuss another approach to the same problem. After all, design patterns are all about approaches. By implementing this design pattern, we allow the user of our code to specify which abilities he or she wants to add.

Enhancing a class Your boss—sorry, scrum master—came to you yesterday with an urgent requirement. From now on, all map data structures in your system are to become HappyMaps. What, you don't know what HappyMaps are? They are the hottest stuff going around right now. They are just like the regular HashMap, but when you override an existing value, they print the following output: Yay! Very useful So, what you do is type the following code in your editor:

 class HappyMap<K, V>: HashMap<K, V>() {

    override fun put(key: K, value: V): V? {

        return super.put(key, value).apply {

            this?.let {

                println("Yay! $key")

            }

        }

    }

}

We've seen apply() already when we discussed the Builder design pattern in the previously and this?.let { ... } is a nicer way of saying if (this != null) { ... }. We can test our solution using the following code:

 fun main(args : Array<String>) {

    val happy = HappyMap<String, String>()

    happy["one"] = "one"

    happy["two"] = "two"

    happy["two"] = "three"

The preceding code prints the following output as expected: Yay! two That was the only overridden key.

Operator overloading 

Hold on a second, how do square brackets keep working when we extended a map? As you may have guessed by the title of this section, Kotlin supports operator overloading. Operator overloading means that the same operator acts differently, depending on the type of arguments it receives. If you've ever worked with Java, you're familiar with operator overloading already. Think of how the plus operator works. Let take a look at the example given here: 

System.out.println(1 + 1); // 2

System.out.println("1" + "1") // 11 Based on whether two arguments are either strings or integers, the + sign acts differently.

But, in the Java world, this is something that only the language itself is allowed to do. The following code won't compile, no matter how hard we try: 

List<String> a = Arrays.asList("a"); List<String> b = Collections.singletonList("b"); // Same for one argument

List<String> c = a + b; In Java 9, there's also List.of(), which serves a similar purpose to Arrays.asList(). In Kotlin, the same code prints [a, b]: val a = listOf("a")

val b = listOf("b")

println(a + b) Well, that makes a lot of sense, but maybe it's just a language feature: data class Json(val j: String)

val j1 = Json("""{"a": "b"}""")

val j2 = Json("""{"c": "d"}""")

println(j1 + j2) // Compilation error! Told you it was magic! You cannot simply join two arbitrary classes together.

But wait. What if we create an extension function for our Json class, plus(), as follows: operator fun Json.plus(j2: Json): Json {

   // Code comes here

Everything but the first keyword, operator, should look familiar to you. We extend the Json object with a new function that gets another Json and returns Json. We implement the function body like this:

 val jsonFields = this.j.split(":") + j2.j.split(":")

val s = (jsonFields).joinToString(":")

return Json ("""{$s}""") 

This isn't really joining any JSON, but it joins Json in our example. We take values from our Json, values from the other Json, then join them together and put some curly brackets around them. Now look at this line: println(j1 + j2) The preceding code prints the following output:

{{"a": "b"}:{"c": "d"}} Actually, it will print: Json(j={{"a": "b"}:{"c": "d"}}).

 This is because we didn't override the toString() method in our example. So, what's this operator keyword about? Unlike some other languages, you cannot override every operator that exists in Kotlin languages, just a chosen few. Albeit limited, the list of all operators that can be overridden is quite long, so we'll not list it here. You can refer to it in the official documentation: 

https://kotlinlang.org/docs/reference/operator-overloading.html. 

Try renaming your extension method to: plus(): Just a name with a typo minus(): The existing function that correlates with the - sign You will see that your code stops compiling.

The square brackets that we started with are called indexed access operators and correlate to the get(x) and set(x, y) methods.

The next day, your product manager reaches out to you. Apparently, they want a SadMap now, which gets sad each time a key is removed from it. Following the previous pattern, you extend the map again:

 class SadMap<K, V>: HashMap<K, V>() {

    override fun remove(key: K): V? {

        println("Okay...")

        return super.remove(key)

    }

But then the chief architect asks that in some cases, a map would be happy and sad at the same time. The CTO already has a great idea for a SuperSadMap that will print the following output twice: Okay... So, what we need is the ability to combine the behaviors of our objects.

The great combinator

 We'll start a bit differently this time. Instead of composing our solution piece by piece, we'll look at the complete solution and decompose it. The code here will help you understand why: 

class HappyMap<K, V>(private 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 {

            this?.let { println("Yay! $key") }

        }

    }

The hardest part here is to understand the signature. What we need in the Decorator pattern is: To be able to receive the object we're decorating To keep a reference to it

When our Decorator is called, we decide if we would like to change the behavior of the object we're holding, or to delegate the call Since we need to actually do a lot of stuff, this declaration is quite complex. After all, it does a lot of stuff in one line, which should be quite impressive. Let's break it down line by line: 

class HappyMap<K, V>(... Our class is named HappyMap and has two type arguments, K and V, which stand for key and value: ... (private val map: MutableMap<K, V> ... 

In our constructor, we receive MutableMap, with types K and V, the same as ours: ... = mutableMapOf()) ... If no map was passed, we initialize our property with the default argument value, which is an empty mutable map: ... : MutableMap<K, V> ... Our class extends the MutableMap interface:

... by map It also delegates all methods that weren't overridden to the object that we will wrap, in our case a map. The code for SadMap using delegate is omitted, but you can easily reproduce it by combining the declaration of HappyMap and the previous implementation of SadMap. 

Let's compose our SadHappyMap now, to please the chief architect:

 val sadHappy = SadMap(HappyMap<String, String>())

sadHappy["one"] = "one"

sadHappy["two"] = "two"

 sadHappy["two"] = "three"

sadHappy["a"] = "b"

sadHappy.remove("a") 

We get the following output: Yay! two // Because it delegates to HappyMap

Okay...  // Because it is a SadMap In the same way, we can now create SuperSadMap:

val superSad = SadMap(HappyMap<String, String>())

 And we can please the CTO too. The Decorator design pattern is widely used in the java.io.* package, with classes such as reader and writer.

Caveats 

The Decorator design pattern is great because it lets us compose objects on the fly. Using Kotlin's by keyword will make it simple to implement. But there are still limitations that you need to take care of. First, you cannot see inside of the Decorator: println(sadHappy is SadMap<*, *>) // True That's the top wrapper, so no problem there: println(sadHappy is MutableMap<*, *>) // True That's the interface we implement, so the compiler knows about it: println(sadHappy is HappyMap<*, *>) // False Although SadMap contains HappyMap and may behave like it, it is not a HappyMap! Keep that in mind while performing casts and type checks. Second, which is related to the first point, is the fact that since Decorator is usually not aware directly of which class it wraps, it's hard to do optimizations. Imagine that our CTO requested SuperSadMap to print Okay... Okay... and that's it, on the same line. For that, we would need to either capture the entire output, or investigate all the classes that we will wrap, which are quite complex tasks. Keep these points in mind when you use this powerful design pattern. It allows for adding new responsibilities to an object dynamically (in our case, printing Yay is a responsibility), instead of subclassing the object. Each new responsibility is a new wrapping layer you add.

Code Example :
DecoratorDesignPattern.kt 

/*class HappyMap<K, V>: HashMap<K, V>() {
override fun put(key: K, value: V): V? {
return super.put(key, value).apply {
this?.let {
println("Yay! $key")
}
}
}
}*/

class SadMap<K, V>(private val map : MutableMap<K, V> = mutableMapOf()) : MutableMap<K, V> by map {
override fun remove(key: K): V? {
println("Okay...")
return map.remove(key)
}
}

class HappyMap<K, V>(private 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")
}
}
}
}
fun main(args: Array<String>) {
// Decorator Design Pattern test
val sadHappy = SadMap(HappyMap<String, String>())
sadHappy["one"] = "one"
sadHappy["two"] = "two"
sadHappy["two"] = "three"
sadHappy["a"] = "b"
sadHappy.remove("a")

println(sadHappy is SadMap<*, *>)
println(sadHappy is HappyMap<*, *>)
println(sadHappy is MutableMap<*, *>)
}

Ouput :

Yay! two

Okay...

true

false

true

No comments: