You get a bonus - 1 coin for daily activity. Now you have 1 coin

The State Design Pattern

Lecture



The "State" pattern – is a vivid example of replacing conditional statements with subtype polymorphism. It is quite widely used and can genuinely reduce code complexity. Let us examine it through the example of how phone screens behave.

Not all phones behave in the same way, but a specific example had to be chosen for the lesson

A phone has three basic states in total:

  1. The phone is off. The screen does not respond to touches.
  2. The phone is on, but the screen is off. The screen responds only to a touch (not to a swipe) and turns on.
  3. The phone is on and so is the screen. The response to touches and gestures depends on the active application.

We will model this logic in the class responsible for the screen, and add two events to it: a touch and a swipe.

class MobileScreen {
  constructor() {
    // At the very start the phone is off
    this.powerOn = false;
    this.screenOn = false;
  }

  // Powering on
  powerOn() {
    this.powerOn = true;
  }

  // Touch
  touch() {
    // If the power is off, nothing happens
    if (!this.powerOn) {
      return;
    }

    // If the screen was off, it needs to be turned on
    if (!this.screenOn) {
      this.screenOn = true;
    }

    // The currently active application should respond to the event
    this.notify('touch');
  }

  // Swipe
  swipe() {
    // If the power or the screen is off, nothing happens
    if (!this.powerOn || !this.screenOn) {
      return;
    }

    // The currently active application should respond to the event
    this.notify('swipe');
  }
}

There are only two events, and yet already so many conditional statements. In reality there would be far more events, and all of them would need to take into account the activity state of the phone and the screen.

Solving this problem head-on gives us a huge number of conditional statements in the method for each event. Such code is very complex and fragile. Changing the number of states and adding new events is fraught with constant bugs. It is hard to see the whole picture and not miss something.

The complexity of such code can be significantly reduced through two successive transformations: extracting an explicit state and introducing subtype polymorphism.

Explicit State

The current implementation of the screen relies on flags. In programming, this is the name given to variables that hold boolean values.

constructor() {
  this.powerOn = false;
  this.screenOn = false;
}

Flags are often (but not always!) a sign of poor architecture. They tend to multiply and overlap. Logic tied to combinations of different flags makes code harder to analyze:

if (!this.powerOn || !this.screenOn) {
  return;
}

This programming style has its own name: "flag programming". That is what people call code that is hard to understand because of logic tied to a combination of flags. And the presence of flags will almost certainly lead to that. The point is that the number of states in a system is, as a rule, more than two. That is, one flag will never be enough.

It is possible to move away from flags by introducing an explicit state for the system. In our example it is easy to notice that there are only three states in total:

  • Power Off: the power is off (which means the screen is off too).
  • Screen Disabled: the screen is off (but the power is on).
  • Screen On: the screen is on.

The next step is to replace the flags with a single variable that stores the current state of the system:

class MobileScreen {
  constructor() {
    this.stateName = 'powerOff';
  }

  powerOn() {
    this.stateName = 'powerOn';
  }

  touch() {
    if (this.stateName === 'powerOff') {
      return;
    }

    if (this.stateName === 'screenDisabled') {
      this.stateName = 'screenOn';
    }

    this.notify('touch');
  }

  swipe() {
    if (this.stateName !== 'screenOn') {
      return;
    }

    // The currently active application should respond to the event
    this.notify('swipe');
  }
}

The main thing that happened in the code above – is that the checks for combinations of flags disappeared. This does not rule out the possibility of checking several states at once, but the states of a system are much easier to understand than sets of flags.

State Classes

To get rid of conditional statements, polymorphism will be needed. What should it be built on? Thanks to having an explicitly extracted state, it is easy to see how behavior depends on the state. It is precisely the states that should be transformed into classes with their own behavior specific to that state.

The screen, in turn, will get rid of all the checks and will start interacting with the states:

import PowerOffState from './states/PowerOffState.js';
import ScreenDisabledState from './states/ScreenDisabledState.js';
import ScreenOnState from './states/ScreenOnState.js';

class MobileScreen {
  constructor() {
    // The list of states is needed for switching between them
    // Otherwise circular dependencies could appear inside the states
    this.states = {
      PowerOff: PowerOffState,
      ScreenDisabled: ScreenDisabledState,
      ScreenOn: ScreenOnState,
    }
    // Initial state
    // The current object is passed inside
    // This is needed to switch states (examples below)
    this.state = new this.states.PowerOff(this);
  }

  powerOn() {
    // The previous state does not matter to us
    // All data is stored in the screen itself
    // State objects do not have their own data
    this.state = new this.states.ScreenDisabled(this);
  }

  touch() {
    this.state.touch();
  }

  swipe() {
    this.state.swipe();
  }
}

// Note that from the point of view of the external code (the user of the screen)
// nothing has changed.

Now the screen does absolutely nothing. All its code — is the initialization of the initial state and passing control to the current active state. So what do the state classes look like?

class PowerOffState {
  constructor(screen) {
    this.screen = screen;
  }

  touch() {
    // nothing happens
  }

  swipe() {
    // nothing happens
  }
}

The state of a powered-off phone is the simplest of all. In this state there is no reaction at all, so the methods are empty. Let us look at ScreenDisabledState:

class ScreenDisabledState {
  constructor(screen) {
    this.screen = screen;
  }

  touch() {
    // Turning on the screen. The screen itself needs to be passed into the constructor.
    this.screen.state = new this.screen.states.ScreenOn(this.screen);
    // Notifying the current app about the activation
    this.screen.notify('touch');
  }

  swipe() {
    // nothing happens
  }
}

A touch on the screen brings it to life. To do this, the ScreenDisabledState state must perform a transition to the ScreenOnState state. This is exactly why the screen itself was passed inside each state. Otherwise it would be impossible to change it.

And the last state is ScreenOnState. This is the only state in which interaction with applications takes place

class ScreenOnState {
  constructor(screen) {
    this.screen = screen;
  }

  touch() {
    this.screen.notify('touch');
  }

  swipe() {
    this.screen.notify('swipe');
  }
}

It is incredible, but there is not a single conditional statement left in the code. It has become easy to see the behavior of the phone for all events in a specific state. It is enough to open the needed class. The price for such convenience – is a larger number of files and code.

It is very important not to miss the main idea of the pattern. State classes are introduced only to introduce polymorphism, but they have no data of their own to work with. Ultimately, all the action is directed at the screen itself, the entity that we are simplifying.

Comments

To leave a comment

If you have any suggestion, idea, thanks or comment, feel free to write. We really value feedback and are glad to hear your opinion.
To reply

Lectures and tutorial on "Object oriented programming"

Terms: Object oriented programming