State Pattern

The pattern allows:

  • to build an architecture where the behavior of an object depends on its state;
  • for an object to partially change its type at run-time.

State Pattern


Let’s take a drawing program. The program has a mouse cursor, which can act as one of several tools. The cursor maintains an internal state representing the tool currently in use. When a tool-dependent method is called, the method call is passed on to the cursor's state. Each tool corresponds to a state.


PHP Example

The logger provided in the example has internal state, which simply saying if the DB connection is still available. The default behavior is to log the message into DB. Though when state is changed (Db unavailable), the behavior changes as well. The logger pushes the message into the file.

<?php
/*
 * @category Design Pattern Tutorial
 * @package State Sample
 * @author Dmitry Sheiko <me@dsheiko.com>
 * @link http://dsheiko.com
 */

/**
 * The state interface and two implementations
 */
interface State_Logging_Interface
{
    public function log(Lib_Logger $context);
}
class State_Logging_File implements State_Logging_Interface
{
    public function log(Lib_Logger $context)
    {
        echo $context->getMessage(), ' logged into file', "\n";
    }
}
class State_Logging_Db implements State_Logging_Interface
{
    private static function _isDbAvailable()
    {
        static $counter = false;
        $counter = !$counter;
        return $counter;
    }
    public function log(Lib_Logger $context)
    {
        if ($this->_isDbAvailable()) {
            echo $context->getMessage(), ' logged into DB', "\n";
        } else {
            $context->setState(new State_Logging_File());
            $context->log($context->getMessage());
            $context->log('DB connection is not available');
        }
    }
}
/**
 * Context class
 */
class Lib_Logger
{
    private $_state;
    private $_message;
    public function  __construct()
    {
        // Default state
        $this->_state = new State_Logging_Db();
    }
    public function setState(State_Logging_Interface $state)
    {
        $this->_state = $state;
    }
    public function getMessage()
    {
        return $this->_message;
    }
    public function log($message )
    {
        $this->_message = $message;
        $this->_state->log($this, $message);
    }
}

/**
 * Usage
 */
$logger = new Lib_Logger();
$logger->log('Message 1');
$logger->log('Message 2');
$logger->log('Message 3');

// Output:
// Message 1 logged into DB
// Message 2 logged into file
// DB connection is not available logged into file
// Message 3 logged into file

JS/ES5 Example

Imagine user interface containing list of users. Above the table there is a select combo-box to choose which operation you would like to apply on the users selected in the list. On-change event on this control element causes change of the widget status. Thus, when you submit the form, the widget will call the proper controller according to the actual widget state.

/*
 * @category Design Pattern Tutorial
 * @package State Sample
 * @author Dmitry Sheiko <me@dsheiko.com>
 * @link http://dsheiko.com
 */
(function(document) {

/**
 * Helps to preserve calling context
 */
var Util = {
    proxy : function (method, context) {
        if (typeof method == 'string') method = context[method];
        var cb = function () {method.apply(context, arguments);}
        return cb;
     }
 };

/**
 * State objects
 */
var ManageUserListBlockState = function(widget) {
    return {
        submit: function() {
            widget.node.form.action = 'BLOCK_CONTROLLER_URI';
            widget.node.form.submit();
        }
    }
}
var ManageUserListDeleteState = function(widget) {
    return {
        submit: function() {
            widget.node.form.action = 'DELETE_CONTROLLER_URI';
            widget.node.form.submit();
        }
    }
}
/**
 * Base constructor for Widgets
 */
var WidgetAbstract = {
    init: function(settings) {
        this.node = {};
        this.node.boundingBox = document.querySelector(settings.boundingBox);
        if (this.hasOwnProperty('PARSE_HTML')) {
            for (var i in this.PARSE_HTML) {
                this.node[i] = document.querySelector(this.PARSE_HTML[i]);
            }
        }
        if (this.hasOwnProperty('syncUI')) {
            this.syncUI();
        }
        if (this.hasOwnProperty('renderUI')) {
            this.renderUI();
        }
    }
}
/**
 * Widget contains submit action depending on states
 */
var ManageUserListWidget = function(settings) {
    var _handler = {
        onUpdateOptionChange : function(e) {
            if (this.node.selectUpdateOptions.value === 'delete') {
                this.setState(new ManageUserListDeleteState(this));
            } else {
                this.setState(new ManageUserListBlockState(this));
            }
        },
        onFormSubmit: function(e) {
            e.preventDefault();
            this.state.submit();
        }
    }
    return {
        state : null,
        init: function() {
            WidgetAbstract.init.call(this, settings);
            this.state = new ManageUserListBlockState(this); // default state
        },
        PARSE_HTML: {
            form : "form",
            selectUpdateOptions: "select"
        },
        syncUI: function() {
            this.node.form.addEventListener('submit', Util.proxy(_handler.onFormSubmit, this), false);
            this.node.selectUpdateOptions.addEventListener('change'
                , Util.proxy(_handler.onUpdateOptionChange, this), false);
        },
        setState: function(state) {
            this.state = state;
        }
    }
}

/**
 * Usage
 */
var widget = new ManageUserListWidget({boundingBox: 'div.userList'});
widget.init();

}(document));