State

State is a variation of the Strategy pattern where the strategy changes depending on the state of the context. We have seen in the previous section how a strategy can be selected based on different variables such as user preferences, a configuration parameter, the input provided and once this selection is done, the strategy stays unchanged for the rest of the lifespan of the context.

In the State pattern instead, the strategy (also called State in this circumstance) is dynamic and can change during the lifetime of the context, thus allowing its behavior to adapt depending on its internal state, as shown in the following figure:

State

Imagine that we have a hotel booking system and an object called Reservation that models a room reservation. This is a classical situation where we have to adapt the behavior of an object based on its state. Consider the following series of events:

  1. When the reservation is initially created, the user can confirm (using confirm()) the reservation; of course, they cannot cancel (using cancel()) it, because it's still not confirmed. They can however delete (using delete()) it if they change their mind before buying.
  2. Once the reservation is confirmed, using the confirm() function again does not make any sense; however, now it should be possible to cancel the reservation but not to delete it any longer, because it has to be kept for the record.
  3. On the day before the reservation date, it should not be possible to cancel the reservation; it's too late for that.

Now, imagine that we have to implement the reservation system that we described in one monolithic object; we can already picture all the if-else or switch statements that we would have to write to enable/disable each action depending on the state of the reservation.

The State pattern instead is perfect in this situation: there will be three strategies and all implementing the three methods described (confirm(), cancel(), delete()) and each one implementing only one behavior, the one corresponding to the modeled state. By using this pattern, it should be very easy for the Reservation object to switch from one behavior to another; this will simply require the activation of a different strategy on each state change.

The state transition can be initiated and controlled by the context object, by the client code, or by the State objects themselves. This last option usually provides the best results in terms of flexibility and decoupling as the context does not have to know about all the possible states and how to transition between them.

Let's now work on a concrete example so that we can apply what we learned about the State pattern. Let's build a client TCP socket that does not fail when the connection with the server is lost; instead, we want to queue all the data sent during the time in which the server is offline and then try to send it again as soon as the connection is re-established. We want to leverage this socket in the context of a simple monitoring system, where a set of machines send some statistics about their resource utilization at regular intervals; if the server that collects these resources goes down, our socket will continue to queue the data locally until the server comes back online.

Let's start by creating a new module called failsafeSocket.js that represents our context object:

The FailsafeSocket pseudo class is made of three main elements:

Let's now see how the two states look like, starting from the offlineState.js module:

The module that we created is responsible for managing the behavior of the socket while it's offline; this is how it works:

  1. Instead of using a raw TCP socket, we will use a little library called json-over-tcp (https://npmjs.org/package/json-over-tcp), which will allow us to easily send JSON objects over a TCP connection.
  2. The send() method is only responsible for queuing any data it receives; we are assuming that we are offline, so that's all we need to do.
  3. The activate() method tries to establish a connection with the server using json-over-tcp. If the operation fails, it tries again after 500 milliseconds. It continues trying until a valid connection is established, in which case the state of failsafeSocket is transitioned to online.

Next, let's implement the onlineState.js module, and then, let's implement the onlineState strategy as follows:

The OnlineState strategy is very simple and is explained as follows:

That's it for our failsafeSocket; now we are ready to build a sample client and a server to try it out. Let's put the server code in a module named server.js:

Then the client side code, which is what we are really interested in, goes into client.js:

Our server simply prints any JSON message it receives to the console, while our clients are sending a measurement of their memory utilization every second, leveraging a FailsafeSocket object.

To try the small system that we built, we should run both the client and the server, then we can test the features of failsafeSocket by stopping and then restarting the server. We should see that the state of the client changes between online and offline, and that any memory measurement collected while the server is offline is queued and then resent as soon as the server goes back online.

This sample should be a clear demonstration of how the State pattern can help increase the modularity and readability of a component that has to adapt its behavior depending on its state.