The state design pattern

The state design pattern is really similar to the strategy design pattern we looked at in the previous chapter. Its purpose is to:

Note

Allow us to choose a different behavior of an object based on the object's internal state.

Basically, the difference between the state design pattern and the strategy design pattern comes from the following two points:

  • The strategy design pattern is about how an action is performed. It is usually an algorithm that produces the same results as other algorithms.
  • The state design pattern is about what action is performed. Depending on the state, an object could be doing different things.

Implementing the state design pattern also closely resembles the implementation of the strategy design pattern.

Class diagram

Imagine a media player. Most media players have a play button—when we activate it, it usually changes its appearance and becomes a pause button. Clicking the pause button now also does something different—it pauses the playback and reverts to a play button. This is a good candidate for the state design pattern, where depending on which state the player is in, a different action happens.

The following class diagram shows the classes that are needed to implement this functionality for the play and pause buttons:

Class diagram

Our Playing and Paused implementations set the state to the opposite one and make our player functional. Using the state design pattern also makes our code much more elegant—we could, of course, use if statements and depending on the value, perform different actions. However, it could easily get out of control when there are many states.

Code example

Let's have a look at the code for the class diagram that we showed previously. First of all, let's see the State trait:

trait State[T] {
  def press(context: T)
}

It is really simple and allows the extending classes to implement the press method. We have two implementations according to our class diagram:

class Playing extends State[MediaPlayer] {
  override def press(context: MediaPlayer): Unit = {
    System.out.println("Pressing pause.")
    context.setState(new Paused)
  }
}

class Paused extends State[MediaPlayer] {
  override def press(context: MediaPlayer): Unit = {
    System.out.println("Pressing play.")
    context.setState(new Playing)
  }
}

We have made them simple and they only print a relevant message and then change the current state to the opposite state.

Our model defines a MediaPlayer class, which looks like the following:

case class MediaPlayer() {
  private var state: State[MediaPlayer] = new Paused
  
  def pressPlayOrPauseButton(): Unit = {
    state.press(this)
  }
  
  def setState(state: State[MediaPlayer]): Unit = {
    this.state = state
  }
}

This really is everything we need. Now we can use our media player in the following application:

object MediaPlayerExample {
  def main(args: Array[String]): Unit = {
    val player = MediaPlayer()
    
    player.pressPlayOrPauseButton()
    player.pressPlayOrPauseButton()
    player.pressPlayOrPauseButton()
    player.pressPlayOrPauseButton()
  }
}

If we run the preceding code, we will see the following output:

Code example

As you can see in the example output, the state changes on every button press and it performs a different action, which we've illustrated using a different print message.

A possible improvement to our application would involve making the state objects singletons. As you can see, they are always the same, so there really is no need to create new ones every single time.

What is it good for?

The state design pattern is really useful for making the code readable and getting rid of conditional statements.

What is it not so good for?

The state design pattern has no major drawbacks. One thing that developers should be careful about is the side effects caused by the change of the state of objects.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.15.245.1