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:
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:
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.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.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:
var OfflineState = require('./offlineState'); var OnlineState = require('./onlineState'); function FailsafeSocket(options) { //[1] this.options = options; this.queue = []; this.currentState = null; this.socket = null; this.states = { offline: new OfflineState(this), online: new OnlineState(this) } this.changeState('offline'); } FailsafeSocket.prototype.changeState = function(state) { //[2] console.log('Activating state: ' + state); this.currentState = this.states[state]; this.currentState.activate(); } FailsafeSocket.prototype.send = function(data) { //[3] this.currentState.send(data); } module.exports = function(options) { return new FailsafeSocket(options); };
The FailsafeSocket
pseudo class is made of three main elements:
states
, one for implementing the behavior of the socket while it's offline and another one when the socket is online.changeState()
method is responsible for transitioning from one state to another. It simply updates the currentState
instance variable and calls activate()
on the target state.send()
method is the functionality of the socket, this is where we want to have a different behavior based on the offline/online state. As we can see, this is done by delegating the operation to the currently active state.Let's now see how the two states look like, starting from the offlineState.js
module:
var jot = require('json-over-tcp'); //[1] function OfflineState(failsafeSocket) { this.failsafeSocket = failsafeSocket; } module.exports = OfflineState; OfflineState.prototype.send = function(data) { //[2] this.failsafeSocket.queue.push(data); } OfflineState.prototype.activate = function() { //[3] var self = this; function retry() { setTimeout(function() { self.activate(); }, 500); } self.failsafeSocket.socket = jot.connect( self.failsafeSocket.options, function() { self.failsafeSocket.socket.removeListener('error', retry); self.failsafeSocket.changeState('online'); } ); self.failsafeSocket.socket.once('error', retry); }
The module that we created is responsible for managing the behavior of the socket while it's offline; this is how it works:
json-over-tcp
(https://npmjs.org/package/json-over-tcp), which will allow us to easily send JSON objects over a TCP connection.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.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:
function OnlineState(failsafeSocket) { this.failsafeSocket = failsafeSocket; } module.exports = OnlineState; OnlineState.prototype.send = function(data) { //[1] this.failsafeSocket.socket.write(data); }; OnlineState.prototype.activate = function() { //[2] var self = this; self.failsafeSocket.queue.forEach(function(data) { self.failsafeSocket.socket.write(data); }); self.failsafeSocket.queue = []; self.failsafeSocket.socket.once('error', function() { self.failsafeSocket.changeState('offline'); }); }
The OnlineState
strategy is very simple and is explained as follows:
send()
method writes the data directly into the socket, as we assume we are online.activate()
method flushes any data that was queued while the socket was offline and it also starts listening for any error
event; we will take this as a symptom that the socket went offline (for simplicity). When this happens, we transition to the offline
state.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
:
var jot = require('json-over-tcp'); var server = jot.createServer(5000); server.on('connection', function(socket) { socket.on('data', function(data){ console.log('Client data', data); }); }); server.listen(5000, function() {console.log('Started')});
Then the client side code, which is what we are really interested in, goes into client.js
:
var createFailsafeSocket = require('./failsafeSocket'); var failsafeSocket = createFailsafeSocket({port: 5000}); setInterval(function() { //send current memory usage failsafeSocket.send(process.memoryUsage()); }, 1000);
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.
The FailsafeSocket
class that we built in this section is only for demonstrating the State pattern and doesn't want to be a complete and 100 percent-reliable solution to handle connectivity issues within TCP sockets. For example, we are not verifying that all the data written into the socket stream is received by the server, which would require some more code not strictly related to the pattern that we wanted to describe.