This post is part of the series SOLID Wash Tunnel.

Definition

"State is a behavioral design pattern that lets an object alter its behavior when its internal state changes. It appears as if the object changed its class."

- Refactoring Guru

The state pattern is closely related to the concept of a Finite State Machine. At any given moment, there is a finite number of states which a class can be in. In each of these states, the class behaves differently, and its state can (or not) be switched from one state to another. The switching of these states are also referrend to as transitions, which are also predetermined.

State machines are often implemented with conditionals (lots of if...else statements) which are known to become very difficult to maintain, as changes to the transition logic may require changing state conditionals in every method.

The state pattern suggests that you create new classes for all possible states of an object and extract all state-specific behaviors into these classes. The original object (known as the context), stores a reference to one of the state objects that represents its current state, and delegates all the state-related work to that object.

It is a bit controversial who's responsibility it is to transition the state, wether it is the context itself or the states themselves. Both can work but it is a trade-off:

  • Context - States are ignorant of each other, but conditionals checks are required to determine the next state.
  • States - No conditionals checks are required, but the states must be aware of each other to determine the next state.

It may very well depend on your use case, but I think neither is good or bad perse. I do like the fact that the state objects can keep a reference to the context, thereby handling state re-transitions of the context depending on the business logic contained within the state objects themselves.

The state pattern enables capabilities like:

  • Eliminating conditional complexities in the code.
  • Encapsulation of behaviours into sepparate classes.
  • Introduction of new states without changing the context.
  • Plays along really well with SRP and OCP principles.

But it also comes with some drawbacks like:

  • More classes, whereby more (albeit easier) code to maintain.
  • Thougher to debug and follow the program flow.
  • Can be overkill if a state machine has only a few states or rarely changes.

UML class diagram. [source]


Implementation

WashTunnel

The WashTunnel class is analogous to Context in the UML diagram. The context WashTunnel stores a reference to the current state object, and delegates to it all state-specific work. The context exposes the State property, as a means to transition to a new state.

Upon initialization, the state of the tunnel is set to AvailableState inside the constructor. The Wash method does not have any specific logic to handle the request, instead it delegates that to the current state object.

Note that marking the setter of the State property to internal would be a good idea, to control state transitions only within the current assembly. That is, through the states as opposed to any calling client code.

public class WashTunnel : IWashTunnel
{
public IWashTunnelState State { get; set; }

public WashTunnel(
ISignalTransmitter transmitter,
IWashStepNotifier notifier)
{
State = new AvailableState(this, transmitter, notifier);
}

public void Wash(IVehicle vehicle, IWashProgram program)
{
State.Handle(vehicle, program);
}
}

IWashTunnelState

The IWashTunnelState interface is analogous to State in the UML diagram. It is an abstraction of a state object that our wash tunnel can be in. It only has one method called Handle which expectes a vehicle to wash, and a wash program to apply.

public interface IWashTunnelState
{
void Handle(IVehicle vehicle, IWashProgram program);
}

AvailableState

The AvailableState class is analogous to ConcreteStateA in the UML diagram. It is an implementation of IWashTunnelState. The Handle method takes care of the actual business logic associated with an available wash tunnel.

The state also accepts the context IWashTunnel. The Handle method is sandwiched between two calls in the context's State property. First we transition the state of the wash tunnel to BusyState (detailed in the next section), then we execute the actual business logic, and at the end we re-transition the state to AvailableState.

public class AvailableState : IWashTunnelState
{
private readonly IWashTunnel _washTunnel;
private readonly ISignalTransmitter _transmitter;
private readonly IWashStepNotifier _notifier;

public AvailableState(
IWashTunnel washTunnel,
ISignalTransmitter transmitter,
IWashStepNotifier notifier)
{
_washTunnel = washTunnel;
_transmitter = transmitter;
_notifier = notifier;
}

public void Handle(IVehicle vehicle, IWashProgram program)
{
if (_washTunnel.State is BusyState)
{
throw new Exception("Only one vehicle can be washed at a time.");
}

// switching state to 'Busy'
_washTunnel.State = new BusyState();

// business logic
IWashStep[] washSteps = program.GetWashSteps().ToArray();

for (int i = 0; i < washSteps.Length - 1; i++)
{
washSteps[i].NextStep(washSteps[i + 1]);
}

washSteps[0].Act(vehicle, (action, status) =>
_notifier.Notify(new WashStepResult(action as IWashStep, status)
));

// switching state to 'Available'
_washTunnel.State = new AvailableState(_washTunnel, _transmitter, _notifier);

// signaling the vehicle is ready for washing
_transmitter.Transmit(new VehicleReadySignal());
}
}

BusyState

The BusyState class is analogous to ConcreteStateB in the UML diagram. It is also an implementation of IWashTunnelState. The wash tunnel can not accept washing a new vehicle, while an other vehicle is being washed from the tunnel. That is why the Handle method is empty! We could extend the functionality to queue up new wash processes, and start them when the previous process has finished, but as of today that functionality is not there (maybe I will add this too, or if you the reader want to contribute, PR's are more than welcomed 🙂).

public class BusyState : IWashTunnelState
{
public void Handle(IVehicle vehicle, IWashProgram program)
{

}
}

Principles


PrincipleAppliedExplanation
SRPAvailableState and BusyState handle business logic that is unique to that specific state, and changes may occur only when the business logic itself changes.
OCPDifferent states can be added, yet the underlying WashTunnel doesn't have to be modified. It simply takes the provided state object and calls upon its Handle method.
LSPYou can set any implementation of IWashTunnelState to the State property exposed by the context.
ISPAvailableState and BusyState both implement the Handle method of IWashTunnelState. At first glance, it may look like the BusyState is violating ISP because of its empty implementation, but in this case it makes sense because that is its job ... "to do nothing" (this can change in the future if we decide to queue up new wash processes).
DIPThe higher level module WashTunnel, does not depend on the lower level modules AvailableState, BusyState. It depends on the abstraction IWashTunnelState.

Continue the series on Visitor Pattern [Comming soon ...]

If you found this article helpful please give it a share in your favorite forums 😉.
The solution project is available at GitHub.