Wednesday, May 24, 2023

State Design Pattern with Kotlin comparative analysis with Java

 State Design Pattern

Lets think of a game and You can think of the State design pattern as an opinionated Strategy, which we discuss at my other blog. But while Strategy is changed from the outside, by the client, the State may change internally, based solely on the input it gets. Look at this dialog a client wrote with Strategy: Client: Here’s a new thing to do, start doing it from now on. Strategy: OK, no problem. Client: What I like about you is that you never argue with me. Compare it with this one: Client: Here’s some new input I got from you. State: Oh, I don't know. Maybe I'll start doing something differently. Maybe not. The client should also expect that the State may even reject some of its inputs: Client: Here's something for you to ponder, State. State: I don't know what it is! Don't you see I'm busy? Go bother some Strategy with this!

So, why do clients still tolerate that State of ours? Well, State is really good at keeping everything under control.

Fifty shades of State 

Carnivorous snails have had enough of this Maronic hero. He throws peas and bananas at them, only to get to another sorry castle. Now they shall act! By default, the snail should stand still to conserve snail energy. But when the hero gets close, it dashes towards him aggressively. If the hero manages to injure it, it should retreat to lick its wounds. Then it will repeat attacking, until one of them is dead. First, we'll declare what can happen during a snail's life: 

interface WhatCanHappen {

 fun seeHero()

 fun getHit(pointsOfDamage: Int)

 fun calmAgain()

}

Our snail implements this interface, so it can get notified of anything that may happen to it and act accordingly:

class Snail : WhatCanHappen {

 private var healthPoints = 10

 override fun seeHero() {

    }

 override fun getHit(pointsOfDamage: Int) {

    }

 override fun timePassed() {

    }

Now, we declare the Mood class, which we mark with the sealed keyword: sealed class Mood {

   // Some abstract methods here, like draw(), for example

}

Sealed classes are abstract and cannot be instantiated. We'll see the benefit of using them in a moment. But before that, let's declare other states: 

 class Still : Mood() 

 class Aggressive : Mood()

 class Retreating : Mood()

 class Dead : Mood() 

 Those are all different states, sorry, moods, of our snail. In State design pattern terms, Snail is the context. It holds the state. So, we declare a member for it:

 class Snail : WhatCanHappen {

    private var mood: Mood = Still()

    // As before

Now let's define what Snail should do when it sees our hero: 

override fun seeHero() {

        mood = when(mood) {

            is Still -> Aggressive()

        }

    } 

Compilation error! Well, that's where the sealed class comes into play. Much like with an enum, Kotlin knows that there's a finite number of classes that extend from it. So, it requires that our when is exhaustive and specifies all different cases in it. If you're using IntelliJ as your IDE, it will even suggest you "add remaining branches" automatically.

Let's describe our state: 

override fun seeHero() {

    mood = when(mood) {

        is Still -> Aggressive()

        is Aggressive -> mood

        is Retreating -> mood

        is Dead -> mood

    }

Of course, else still works: 

override fun timePassed() {

    mood = when(mood) {

        is Retreating -> Aggressive()

        else -> mood

    }

When the snail gets hit, we need to decide if it's dead or not. For that, we can use when without an argument:

override fun getHit(pointsOfDamage: Int) {

    healthPoints -= pointsOfDamage

     mood = when {

        (healthPoints <= 0) -> Dead()

        mood is Aggressive -> Retreating()

        else -> mood

    }

Note that we use the is keyword, which is the same as instanceof in Java, but more concise.

State of the Nation The previous approach has most of the logic in our context. You may sometimes see a different approach, which is valid as your context grows bigger. In this approach, Snail would become really thin:

class Snail {

    internal var mood: Mood = Still(this)

 private var healthPoints = 10

    // That's all!

Note that we marked mood as internal. That lets other classes in that package alter it. Instead of Snail implementing WhatCanHappen, our Mood will: sealed class Mood : WhatCanHappen And now the logic resides within our state objects: 

class Still(private val snail: Snail) : Mood() {

    override fun seeHero() = snail.mood.run {

         Aggressive(snail)

    }

 override fun getHit(pointsOfDamage: Int) = this override fun timePassed() = this

 } 

Note that our state objects now receive a reference to their context in the constructor. That's the first time we've met the run extension function. It's equivalent would be:

 override fun seeHero(): Mood {

    snail.mood = Aggressive(snail)

    return snail.mood

By using run, we can preserve the same logic, but omit the function body. You'll need to decide what approach to use. In our example, this will actually produce much more code, will have to implement all the methods by itself.

Code Example :

package designpatterns


fun main(args: Array<String>) {
// call from here
}

class Snail : WhatCanHappen {
private var mood: Mood = Still()
private var healthPoints = 10

override fun seeHero() {
mood = when (mood) {
is Still -> Aggressive()
is Aggressive -> mood
is Retreating -> mood
is Dead -> mood
}
}

override fun getHit(pointsOfDamage: Int) {
healthPoints -= pointsOfDamage

mood = when {
(healthPoints <= 0) -> Dead()
mood is Aggressive -> Retreating()
else -> mood
}
}

override fun timePassed() {
mood = when (mood) {
is Retreating -> Aggressive()
else -> mood
}
}
}

interface WhatCanHappen {
fun seeHero()
fun getHit(pointsOfDamage: Int)
fun timePassed()
}

sealed class Mood {}

class Still : Mood()

class Aggressive : Mood()

class Retreating : Mood()

class Dead : Mood()

Note : this game I designed based on State Design Pattern.

No comments: