avatarPeterson C

Summarize

A guide to software design patterns

Behavioral Design Pattern: State

Photo by Fabian Grohs on Unsplash

The state pattern is one of the eleven current behavioral software design patterns. It allows an object to change its behavior when its internal state changes. This pattern makes it easy to change an object’s behavior without having to check the current of the object with a conditional statement, which makes a codebase flexible, testable and maintainable.

Pattern Ideal Usage

When should this pattern be used? Let’s say you’re in charge of designing a system that changes the input of an audio device. There are multiple inputs we can change to but the switch happens in sequential order ( e.g. Bluetooth, Optical, Coaxial, RCA and USB). One input will always be selected.

This scenario would be a good use of the state pattern. It doesn’t matter what’s being done underneath the hood of the audio device, we just care that it changes from one input (state) to the next when we hit the switch button.

Implementation

What would that look like in code? Each state would perform some work to transition to the new state — i.e. to switch from Bluetooth → Optical → Coaxial → RCA → USB inputs. Each one of these inputs is responsible for switching to the next one. Then there’s a context class that keeps track of the current input and allows the switch to the next input.

We’d start with an interface named InputState to capture common behavior (s) of each state. In our case, we will only need to change input, so we’d add a changeInput() method.

public interface InputState {
    void changeInput(InputContext context);
}

Then we need to create a class for each state that implements the InputState interface. These classes need to be Singleton and allow their consumers to get an instance via a static method. They will look similar because we’re not doing anything complex when changing state. Here are the code snippets for our classes.

BluetoothState:

class BluetoothState implements InputState{
    private final static BluetoothState instance = new BluetoothState();
    //to prevent instantiation outside
    private BluetoothState(){}
    public static BluetoothState getInstance(){
        return instance;
    }
    @Override
    public void changeInput(final InputContext context) {
        System.out.println("Switching input to Optical");
        context.setState(OpticalState.getInstance());
    }
}

OpticalState:

public class OpticalState implements InputState{
    private final static OpticalState instance = new OpticalState();
    //to prevent instantiation outside
    private OpticalState(){}
    public static OpticalState getInstance(){
        return instance;
    }
    @Override
    public void changeInput(InputContext context) {
        System.out.println("Switching input to Coaxial");
        context.setState(CoaxialState.getInstance());
    }
}

CoaxialState:

public class CoaxialState implements InputState{
    private final static CoaxialState instance = new CoaxialState();
    //to prevent instantiation outside
    private CoaxialState(){}
    public static CoaxialState getInstance(){
        return instance;
    }
    @Override
    public void changeInput(InputContext context) {
        System.out.println("Switching input to RCA");
        context.setState(RCAState.getInstance());
    }
}

RCAState:

public class RCAState implements InputState{
    private final static RCAState instance = new RCAState();
    //to prevent instantiation outside
    private RCAState(){}
    public static RCAState getInstance(){
        return instance;
    }
    @Override
    public void changeInput(InputContext context) {
        System.out.println("Switching input to USB");
        context.setState(USBState.getInstance());
    }
}

USBState:

public class USBState implements InputState{
    private final static USBState instance = new USBState();
    //to prevent instantiation outside
    private USBState(){}
    public static USBState getInstance(){
        return instance;
    }
    @Override
    public void changeInput(InputContext context) {
        System.out.println("Switching input to Bluetooth");
        context.setState(BluetoothState.getInstance());
    }
}

The InputContext class ties it all together. It allows us to set and change the current state — i.e switching from one input to the next by calling the changeState() method.

The soundbar we’re simulating defaults to Bluetooth input the first time it’s used, then cycle through the states thereafter. We could pass in a state when we create a new context and go from there. For instance, if you were designing a system with a start and a completed step, then you wouldn’t cycle through, but would still need to define the sequential order.

Here’s the context class code.

public class InputContext {
    private InputState state;
    public InputContext()
    {
        this.state = BluetoothState.getInstance();
    }
    
    public void setState(final InputState state) {
        this.state = state;
    }
    public void changeState() {
        this.state.changeInput(this);
    }
    public InputState getState() {
        return this.state;
    }
}

We can use our InputContext to cycle through inputs like this.

public static void main(String[] args)  {
    InputContext context = new InputContext();
    context.changeState();
    context.changeState();
    context.changeState();
    context.changeState();
    context.changeState();
}

Your output now looks like this:

Switching input to Optical
Switching input to Coaxial
Switching input to RCA
Switching input to USB
Switching input to Bluetooth

That’s pretty much the idea behind the state design pattern. I can’t wait to hear about some cool solution you implemented using this pattern.

Thank you for making it to the end. Happy coding.

Previous: Behavioral Design Pattern: Memento

Next: Behavioral Design Pattern: Strategy

Programming
Java
Software Engineering
Software Development
Design Patterns
Recommended from ReadMedium