State

State machines are an amazingly useful device in computer programming. Unfortunately they are not used very frequently by most programmers. I'm sure that at least some of the objection to state machines is that many people implement them as a giant if statement like so:

function (action, amount) {
  if (this.state == "overdrawn" && action == "withdraw") {
    this.state = "on hold";
  }
  if (this.state == "on hold" && action != "deposit") {
    this.state = "on hold";
  }
  if (this.state == "good standing" && action == "withdraw" && amount <= this.balance) {
    this.balance -= amount;
  }
  if (this.state == "good standing" && action == "withdraw" && amount >this.balance) {
    this.balance -= amount;
    this.state = "overdrawn";
  }
};

This is just a sample of what could be much longer. The if statements of this length are painful to debug and highly error prone. Simply flipping a greater than sign is enough to drastically change how the if statement works.

Instead of using a single giant if statement block we can make use of the state pattern. The state pattern is characterized by having a state manager which abstracts away the internal state and proxies a message through to the appropriate state which is implemented as a class. All the logic within states and governing state transitions is governed by the individual state classes. The state manager pattern can be seen in the following diagram:

State

Splitting state into a class per state allows for much smaller blocks of code to debug and makes testing much easier.

The interface for the state manager is fairly simple and usually just provides the methods needed to communicate with the individual states. The manager may also contain some shared state variables.

Implementation

As alluded to in the if statement example, Westeros has a banking system. Much of it is centered on the island of Braavos. Banking there runs in much the same way as banking here, with accounts, deposits, and withdrawals. Managing the state of a bank account involves keeping an eye on all of the transactions and changing the state of the bank account in accordance with the transactions.

Let's take a look at some of the code which is needed to manage a bank account at the Iron Bank of Braavos. First is the state manager:

class BankAccountManager {
  constructor() {
    this.currentState = new GoodStandingState(this);
  }
  Deposit(amount) {
    this.currentState.Deposit(amount);
  }
  Withdraw(amount) {
    this.currentState.Withdraw(amount);
  }
  addToBalance(amount) {
    this.balance += amount;
  }
  getBalance() {
    return this.balance;
  }
  moveToState(newState) {
    this.currentState = newState;
  }
}

The BankAccountManager class provides a state for the current balance and also the current state. To protect the balance, it provides an accessory for reading the balance and another for adding to the balance. In a real banking application, I would rather expect the function that sets the balance, have more protection than this. In this version of BankManager, the ability to manipulate the current state is accessible to the states. They have the responsibility to change states. This functionality can be centralized in the manager but that increases the complexity of adding new states.

We've identified three simple states for the bank account: Overdrawn, OnHold, and GoodStanding. Each one is responsible for dealing with withdrawals and deposits when in that state. The GoodStandingstate class looks like the following:

class GoodStandingState {
  constructor(manager) {
    this.manager = manager;
  }
  Deposit(amount) {
    this.manager.addToBalance(amount);
  }
  Withdraw(amount) {
    if (this.manager.getBalance() < amount) {
      this.manager.moveToState(new OverdrawnState(this.manager));
    }
    this.manager.addToBalance(-1 * amount);
  }
}

The OverdrawnState class looks like the following:

class OverdrawnState {
  constructor(manager) {
    this.manager = manager;
  }
  Deposit(amount) {
    this.manager.addToBalance(amount);
    if (this.manager.getBalance() > 0) {
      this.manager.moveToState(new GoodStandingState(this.manager));
    }
  }
  Withdraw(amount) {
    this.manager.moveToState(new OnHold(this.manager));
    throw "Cannot withdraw money from an already overdrawn bank account";
  }
}

Finally, the OnHold state looks like the following:

class OnHold {
  constructor(manager) {
    this.manager = manager;
  }
  Deposit(amount) {
    this.manager.addToBalance(amount);
    throw "Your account is on hold and you must attend the bank to resolve the issue";
  }
  Withdraw(amount) {
    throw "Your account is on hold and you must attend the bank to resolve the issue";
  }
}

You can see that we've managed to reproduce all the logic of the confusing if statement in a number of simple classes. The amount of code here looks to be far more than the if statement but, in the long run, encapsulating the code into individual classes will pay off.

There is plenty of opportunity to make use of this pattern within JavaScript. Keeping track of state is a typical problem in most applications. When the transitions between the states are complex, then wrapping it up in a state pattern is one method of simplifying things. It is also possible to build up a simple workflow by registering events as sequential. A nice interface for this might be a fluent one so that you could register states like the following:

goodStandingState
.on("withdraw")
.when(function(manager){return manager.balance > 0;})
  .transitionTo("goodStanding")
.when(function(manager){return mangaer.balance <=0;})
  .transitionTo("overdrawn");
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset