DEV Community

Oluwasanmi Aderibigbe
Oluwasanmi Aderibigbe

Posted on

Head First Design Patterns : 9 of 10 ( State Pattern)

I just learnt a new design from the Head First Design Pattern book. Today, I learnt about the State pattern.

According to The Head first design pattern, The State pattern allows an object to alter its behaviour when its internal state changes. The object will appear to change its class. The state pattern achieves this by creating a finite number of state classes for the object and also events that allow the object changes its state. This allows the object to delegate calls to different state classes. In turn, changing the behaviour of the object.

Implementing the state pattern starts with:

  1. Creating your context class. The context class is the class whose behaviour you'd like to change using the State pattern at runtime.
  2. Identifying the different states of your object and also events. Events are actions that changes the state of your object.
  3. Defining an abstract state base. It can be an abstract or a Marker interface.
  4. Create the state classes.

For example, we are going to implement an program that fetches upcoming film titles. To implement this using the state pattern. The first thing we need to do is create our context and identify the different states of the programme and our it transitions from one state to another.

class UpcomingTitlesFetcher {
private var state: State = loadingState
}
Enter fullscreen mode Exit fullscreen mode

UpcomingTitlesFetcher is our context. It holds a reference to the State interface.

The UpcomingTitlesFetcher has four states; Initial, Loading, Success, and Failure.


sealed interface State {
    fun showInitial()
    fun showLoading()
    fun showUpcomingTitles()
    fun showFailure()

    class Initial(val upcomingTitlesFetcher: UpcomingTitlesFetcher) : State {
        init {
            upcomingTitlesFetcher.setState(this)
        }

        override fun showInitial() {
            upcomingTitlesFetcher.setState(Initial(upcomingTitlesFetcher))
        }

        override fun showLoading() {
            upcomingTitlesFetcher.setState(LoadingState(upcomingTitlesFetcher))
        }

        override fun showUpcomingTitles() {
            throw IllegalStateException()
        }

        override fun showFailure() {
            throw IllegalStateException()
        }

    }

    class LoadingState(val upcomingTitlesFetcher: UpcomingTitlesFetcher) : State {
        init {
            upcomingTitlesFetcher.setState(this)
        }

        override fun showInitial() {
            throw IllegalStateException()
        }

        override fun showLoading() {
            upcomingTitlesFetcher.setState(LoadingState(upcomingTitlesFetcher))
        }

        override fun showUpcomingTitles() {
            val titles = getTitles()
            upcomingTitlesFetcher.setState(SucessState(upcomingTitlesFetcher, titles))
        }

        override fun showFailure() {
            upcomingTitlesFetcher.setState(FailedState(upcomingTitlesFetcher))
        }

    }

    class SucessState(val upcomingTitlesFetcher: UpcomingTitlesFetcher, val list: List<String> = emptyList()) : State {
        override fun showInitial() {
            upcomingTitlesFetcher.setState(Initial(upcomingTitlesFetcher))
        }

        override fun showLoading() {
            throw IllegalStateException()
        }

        override fun showUpcomingTitles() {
            throw IllegalStateException()
        }

        override fun showFailure() {
            throw IllegalStateException()
        }

    }

    class FailedState(val upcomingTitlesFetcher: UpcomingTitlesFetcher ) : State {
        override fun showInitial() {
            upcomingTitlesFetcher.setState(Initial(upcomingTitlesFetcher))
        }

        override fun showLoading() {
            throw IllegalStateException()
        }

        override fun showUpcomingTitles() {
            throw IllegalStateException()
        }

        override fun showFailure() {
            throw IllegalStateException()
        }

    }
}

Enter fullscreen mode Exit fullscreen mode

The code above defines a marker interface called State. State has four methods that defines the transition from one state to another. We also four state classes that implement the State interface. The state class themselves hold a reference to the context and define the state of the context.

For example, in the loading state below. When the showInitial is called an IllegalStateException is called because the programme is already in the loading state. It doesn't make any sense for the programme to switch to the initial state whilst in the loading state. It makes more sense to either transition to the success or failure state.

 class LoadingState(val upcomingTitlesFetcher: UpcomingTitlesFetcher) : State {
        init {
            upcomingTitlesFetcher.setState(this)
        }

        override fun showInitial() {
            throw IllegalStateException()
        }

        override fun showLoading() {
            upcomingTitlesFetcher.setState(LoadingState(upcomingTitlesFetcher))
        }

        override fun showUpcomingTitles() {
            val titles = getTitles()
            upcomingTitlesFetcher.setState(SucessState(upcomingTitlesFetcher, titles))
        }

        override fun showFailure() {
            upcomingTitlesFetcher.setState(FailedState(upcomingTitlesFetcher))
        }
    }
Enter fullscreen mode Exit fullscreen mode

Now we include our states classes into the UpcomingTitlesFetcher.

class UpcomingTitlesFetcher {
    val intialState : State.Initial = State.Initial(this)
    val loadingState: State.LoadingState = State.LoadingState(this)
    val sucessResultState: State.SucessState = State.SucessState(this)
    val failureState: State.FailedState = State.FailedState(this)

    private var state: State = intialState

    fun getUpcomingTitles() {
        state.showInitial()
        println(state)
        state.showLoading()
        println(state)
        state.showUpcomingTitles()
        println(state)
    }

    private fun setState(state: State) {
        this.state = state
    }
}
Enter fullscreen mode Exit fullscreen mode

I'll have to admit with this example, using the state pattern is a bit too much. There are two ways of changing states in the State pattern. Either the State classes change the state of the context or the Context itself changes its state.

Using the second approach, since the context is going to determine when to switch states. The State class can be refactored to this.

    sealed interface State2 {
        object Initial : State2
        object Loading : State2
        class Success(val data: List<String>) : State2
        class Failure(val error: String) : State2
    }

class UpcomingTitlesFetcher2 {
    private var state: State2 = State2.Initial

    fun getUpcomingTitles() {
        println(state)
        state = State2.Loading
        println(state)
        val titles = getTitles()
        state = State2.Success(titles)
        println(state)
    }

Enter fullscreen mode Exit fullscreen mode

In my opinion, they both have their disadvantages and advantages. For simple states, like this one. I'd go with the second approach. For complicated states and events, I would prefer to encapsulate the state logic into different classes and have them change the state of the context.

Discussion (0)