Both Proxy and State
provide a surrogate class that you use in your code; the real class that does
the work is hidden behind this surrogate class. When you call a method in the
surrogate, it simply turns around and calls the method in the implementing
class. These two patterns are so similar that the Proxy is simply a
special case of State. One is tempted to just lump the two together into
a pattern called Surrogate, but the term “proxy” has a
long-standing and specialized meaning, which probably explains the reason for
the two different patterns.
The basic idea is simple: from a base
class, the surrogate is derived along with the class or classes that provide the
actual implementation:
When a surrogate object is created, it is
given an implementation to which to send all of the method
calls.
Structurally, the difference between
Proxy and State is simple: a Proxy has only one
implementation, while State has more than one. The application of the
patterns is considered (in Design Patterns) to be distinct: Proxy
is used to control access to its implementation, while State allows you
to change the implementation dynamically. However, if you expand your notion of
“controlling access to implementation” then the two fit neatly
together.
If we implement Proxy by following
the above diagram, it looks like this:
//: c04:ProxyDemo.java // Simple demonstration of the Proxy pattern. import com.bruceeckel.test.*; interface ProxyBase { void f(); void g(); void h(); } class Proxy implements ProxyBase { private ProxyBase implementation; public Proxy() { implementation = new Implementation(); } // Pass method calls to the implementation: public void f() { implementation.f(); } public void g() { implementation.g(); } public void h() { implementation.h(); } } class Implementation implements ProxyBase { public void f() { System.out.println("Implementation.f()"); } public void g() { System.out.println("Implementation.g()"); } public void h() { System.out.println("Implementation.h()"); } } public class ProxyDemo extends UnitTest { Proxy p = new Proxy(); public void test() { // This just makes sure it will complete // without throwing an exception. p.f(); p.g(); p.h(); } public static void main(String args[]) { new ProxyDemo().test(); } } ///:~
Of course, it isn’t
necessary that Implementation have the same interface as Proxy; as
long as Proxy is somehow “speaking for” the class that it is
referring method calls to then the basic idea is satisfied (note that this
statement is at odds with the definition for Proxy in GoF). However, it is
convenient to have a common interface so that Implementation is forced to
fulfill all the methods that Proxy needs to
call.
The State pattern adds more
implementations to Proxy, along with a way to switch from one
implementation to another during the lifetime of the surrogate:
//: c04:StateDemo.java // Simple demonstration of the State pattern. import com.bruceeckel.test.*; interface StateBase { void f(); void g(); void h(); } class State_d implements StateBase { private StateBase implementation; public State_d(StateBase imp) { implementation = imp; } public void changeImp(StateBase newImp) { implementation = newImp; } // Pass method calls to the implementation: public void f() { implementation.f(); } public void g() { implementation.g(); } public void h() { implementation.h(); } } class Implementation1 implements StateBase { public void f() { System.out.println("Implementation1.f()"); } public void g() { System.out.println("Implementation1.g()"); } public void h() { System.out.println("Implementation1.h()"); } } class Implementation2 implements StateBase { public void f() { System.out.println("Implementation2.f()"); } public void g() { System.out.println("Implementation2.g()"); } public void h() { System.out.println("Implementation2.h()"); } } public class StateDemo extends UnitTest { static void run(State_d b) { b.f(); b.g(); b.h(); } State_d b = new State_d(new Implementation1()); public void test() { // This just makes sure it will complete // without throwing an exception. run(b); b.changeImp(new Implementation2()); run(b); } public static void main(String args[]) { new StateDemo().test(); } } ///:~
In main( ), you
can see that the first implementation is used for a bit, then the second
implementation is swapped in and that is used.
The difference between Proxy and
State is in the problems that are solved. The common uses for
Proxy as described in Design Patterns are:
You could look at a
Java reference as a kind of protection proxy, since it controls access to the
actual object on the heap (and ensures, for example, that you don’t use a
null reference).
[[ Rewrite this: In Design
Patterns, Proxy and State are not seen as related to each
other because the two are given (what I consider arbitrarily) different
structures. State, in particular, uses a separate implementation
hierarchy but this seems to me to be unnecessary unless you have decided that
the implementation is not under your control (certainly a possibility, but if
you own all the code there seems to be no reason not to benefit from the
elegance and helpfulness of the single base class). In addition, Proxy
need not use the same base class for its implementation, as long as the proxy
object is controlling access to the object it “fronting” for.
Regardless of the specifics, in both Proxy and State a surrogate
is passing method calls through to an implementation
object.]]]
While State has a way to allow the
client programmer to change the implementation, StateMachine imposes a
structure to automatically change the implementation from one object to the
next. The current implementation represents the state that a system is in, and
the system behaves differently from one state to the next (because it uses
State). Basically, this is a “state machine” using
objects.
The code that moves the system from one
state to the next is often a Template Method, as seen in the following
framework for a basic state machine. We start by defining a tagging interface
for input objects:
//: c04:statemachine:Input.java // Inputs to a state machine package c04.statemachine; public interface Input {} ///:~
Each
state can be run( ) to perform its behavior, and (in this design)
you can also pass it an Input object so it can tell you what new state to
move to based on that Input. The key distinction between this design and
the next is that here, each State object decides what other states it can
move to, based on the Input, whereas in the subsequent design all of the
state transitions are held in a single table. Another way to put it is that
here, each State object has its own little State table, and in the
subsequent design there is a single master state transition table for the whole
system.
//: c04:statemachine:State.java // A State has an operation, and can be moved // into the next State given an Input: package c04.statemachine; public interface State { void run(); State next(Input i); } ///:~
The StateMachine
keeps track of the current state, which is initialized by the constructor. The
runAll( ) method takes an Iterator to a list of Input
objects (an Iterator is used here for convenience and simplicity; the
important issue is that the input information comes from somewhere). This method
not only moves to the next state, but it also calls run( ) for each
state object – thus you can see it’s an expansion of the idea of the
State pattern, since run( ) does something different
depending on the state that the system is in.
//: c04:statemachine:StateMachine.java // This state machine takes an Iterator of Inputs // to move from State to State using a template // method. package c04.statemachine; import java.util.*; public abstract class StateMachine { private State currentState; public StateMachine(State initialState) { currentState = initialState; currentState.run(); } // Template method: public final void runAll(Iterator inputs) { while(inputs.hasNext()) { Input i = (Input)inputs.next(); System.out.println(i); currentState = currentState.next(i); currentState.run(); } } } ///:~
I’ve also treated
runAll( ) as a template method. This is typical, but certainly not
required – you could concievably want to override it, but typically the
behavior change will occur in State’s run( )
instead.
At this point the basic framework for
this style of StateMachine (where each state decides the next states) is
complete. As an example, I’ll use a fancy mousetrap that can move through
several states in the process of trapping a
mouse[9]. The mouse
classes and information are stored in the mouse package, including a
class representing all the possible moves that a mouse can make, which will be
the inputs to the state machine:
//: c04:mouse:MouseAction.java // This state machine takes an Iterator of Inputs // to move from State to State using a template // method. package c04.mouse; import c04.statemachine.*; public class MouseAction implements Input { private String action; public MouseAction(String a) { action = a; } public String toString() { return action; } public int hashCode() { return action.hashCode(); } public boolean equals(Object o) { return (o instanceof MouseAction) && action.equals(((MouseAction)o).action); } public static MouseAction appears = new MouseAction("mouse appears"), runsAway = new MouseAction("mouse runs away"), enters = new MouseAction("mouse enters trap"), escapes = new MouseAction("mouse escapes"), trapped = new MouseAction("mouse trapped"), removed = new MouseAction("mouse removed"); } ///:~
You’ll note that
hashCode( ) and equals( ) have been overriden so that
MouseAction objects can be used as keys in a HashMap, but in the
first version of the mousetrap we won’t do this. Also, each possible move
by a mouse is enumerated as a static MouseAction object.
For creating test code, a sequence of
mouse inputs is provided from a text file:
//:! c04:mouse:MouseMoves.txt mouse appears mouse runs away mouse appears mouse enters trap mouse escapes mouse appears mouse enters trap mouse trapped mouse removed mouse appears mouse runs away mouse appears mouse enters trap mouse trapped mouse removed ///:~
To read this file in a generic
fashion, here is a general-purpose tool called
StringList:
//: com:bruceeckel:util:StringList.java // General-purpose tool that reads a file of text // lines into a List, one line per list. package com.bruceeckel.util; import java.io.*; import java.util.*; public class StringList extends ArrayList { public StringList(String textFile) { try { BufferedReader inputs = new BufferedReader ( new FileReader(textFile)); String line; while((line = inputs.readLine()) != null) add(line.trim()); } catch (IOException e) { e.printStackTrace(System.err); } } } ///:~
This StringList only
holds Objects, just as an ArrayList does, so we need an adapter to
turn the Strings into MouseActions:
//: c04:mouse:MouseMoveList.java // A transformer to produce a // List of MouseAction objects. package c04.mouse; import java.util.*; import com.bruceeckel.util.*; public class MouseMoveList extends ArrayList { public MouseMoveList(Iterator sit) { while(sit.hasNext()) add(new MouseAction((String)sit.next())); } } ///:~
The MouseMoveList
looks a bit like a decorator, and acts a bit like an adapter. However, an
adapter changes one interface to another, and a decorator adds functionality or
data. MouseMoveList changes the contents of the container, so it might be
thought of as a Transformer.
With these tools in place, it’s now
possible to create the first version of the mousetrap program. Each State
subclass defines it’s run( ) behavior, and also establishes
its next state with an if-else clause:
//: c04:mousetrap1:MouseTrapTest.java // State Machine pattern using 'if' statements // to determine the next state. package c04.mousetrap1; import c04.mouse.*; import c04.statemachine.*; import com.bruceeckel.util.*; import java.util.*; import java.io.*; import com.bruceeckel.test.*; // A different subclass for each state: class Waiting implements State { public void run() { System.out.println( "Waiting: Broadcasting cheese smell"); } public State next(Input i) { MouseAction ma = (MouseAction)i; if(ma.equals(MouseAction.appears)) return MouseTrap.luring; return MouseTrap.waiting; } } class Luring implements State { public void run() { System.out.println( "Luring: Presenting Cheese, door open"); } public State next(Input i) { MouseAction ma = (MouseAction)i; if(ma.equals(MouseAction.runsAway)) return MouseTrap.waiting; if(ma.equals(MouseAction.enters)) return MouseTrap.trapping; return MouseTrap.luring; } } class Trapping implements State { public void run() { System.out.println("Trapping: Closing door"); } public State next(Input i) { MouseAction ma = (MouseAction)i; if(ma.equals(MouseAction.escapes)) return MouseTrap.waiting; if(ma.equals(MouseAction.trapped)) return MouseTrap.holding; return MouseTrap.trapping; } } class Holding implements State { public void run() { System.out.println("Holding: Mouse caught"); } public State next(Input i) { MouseAction ma = (MouseAction)i; if(ma.equals(MouseAction.removed)) return MouseTrap.waiting; return MouseTrap.holding; } } class MouseTrap extends StateMachine { public static State waiting = new Waiting(), luring = new Luring(), trapping = new Trapping(), holding = new Holding(); public MouseTrap() { super(waiting); // Initial state } } public class MouseTrapTest extends UnitTest { MouseTrap trap = new MouseTrap(); MouseMoveList moves = new MouseMoveList( new StringList("../mouse/MouseMoves.txt") .iterator()); public void test() { trap.runAll(moves.iterator()); } public static void main(String args[]) { new MouseTrapTest().test(); } } ///:~
The StateMachine
class simply defines all the possible states as static objects, and also sets up
the initial state. The UnitTest creates a MouseTrap and then tests
it with all the inputs from a MouseMoveList.
While the use
of if-else statements inside the next( ) methods is perfectly
reasonable, managing a large number of these could become difficult. Another
approach is to create tables inside each State object defining the
various next states based on the input.
Initially, this seems like it ought to be
quite simple. You should be able to define a static table in each
State subclass that defines the transitions in terms of the other
State objects. However, it turns out that this approach generates cyclic
initialization dependencies. To solve the problem, I’ve had to delay the
initialization of the tables until the first time that the next( )
method is called for a particular State object. Initially, the
next( ) methods can appear a little strange because of
this.
The StateT class is an
implementation of State (so that the same StateMachine class can
be used from the previous example) that adds a Map and a method to
initialize the map from a two-dimensional array. The next( ) method
has a base-class implementation which must be called from the overridden derived
class next( ) methods after they test for a null Map (and
initialize it if it’s null):
//: c04:mousetrap2:MouseTrap2Test.java // A better mousetrap using tables package c04.mousetrap2; import c04.mouse.*; import c04.statemachine.*; import java.util.*; import java.io.*; import com.bruceeckel.util.*; import com.bruceeckel.test.*; abstract class StateT implements State { protected Map transitions = null; protected void init(Object[][] states) { transitions = new HashMap(); for(int i = 0; i < states.length; i++) transitions.put(states[i][0], states[i][1]); } public abstract void run(); public State next(Input i) { if(transitions.containsKey(i)) return (State)transitions.get(i); else throw new RuntimeException( "Input not supported for current state"); } } class MouseTrap extends StateMachine { public static State waiting = new Waiting(), luring = new Luring(), trapping = new Trapping(), holding = new Holding(); public MouseTrap() { super(waiting); // Initial state } } class Waiting extends StateT { public void run() { System.out.println( "Waiting: Broadcasting cheese smell"); } public State next(Input i) { // Delayed initialization: if(transitions == null) init(new Object[][] { { MouseAction.appears, MouseTrap.luring }, }); return super.next(i); } } class Luring extends StateT { public void run() { System.out.println( "Luring: Presenting Cheese, door open"); } public State next(Input i) { if(transitions == null) init(new Object[][] { { MouseAction.enters, MouseTrap.trapping }, { MouseAction.runsAway, MouseTrap.waiting }, }); return super.next(i); } } class Trapping extends StateT { public void run() { System.out.println("Trapping: Closing door"); } public State next(Input i) { if(transitions == null) init(new Object[][] { { MouseAction.escapes, MouseTrap.waiting }, { MouseAction.trapped, MouseTrap.holding }, }); return super.next(i); } } class Holding extends StateT { public void run() { System.out.println("Holding: Mouse caught"); } public State next(Input i) { if(transitions == null) init(new Object[][] { { MouseAction.removed, MouseTrap.waiting }, }); return super.next(i); } } public class MouseTrap2Test extends UnitTest { MouseTrap trap = new MouseTrap(); MouseMoveList moves = new MouseMoveList( new StringList("../mouse/MouseMoves.txt") .iterator()); public void test() { trap.runAll(moves.iterator()); } public static void main(String args[]) { new MouseTrap2Test().test(); } } ///:~
The rest of the code is
identical – the difference is in the next( ) methods and the
StateT class.
If you have to create and maintain a lot
of State classes, this approach is an improvement, since it’s
easier to quickly read and understand the state transitions from looking at the
table.
The advantage of the previous design is
that all the information about a state, including the state transition
information, is located within the state class itself. This is generally a good
design principle.
However, in a pure state machine, the
machine can be completely represented by a single state-transition table. This
has the advantage of locating all the information about the state machine in a
single place, which means that you can more easily create and maintain the table
based on a classic state-transition diagram.
The classic state-transition diagram uses
a circle to represent each state, and lines from the state pointing to all
states that state can transition into. Each transition line is annotated with
conditions for transition and an action during transition. Here’s what it
looks like:
(Simple State Machine
Diagram)
Goals:
Observations:
Example:
The State class is distinctly
different from before, since it is really just a placeholder with a name. Thus
it is not inherited from previous State classes:
//: c04:statemachine2:State.java package c04.statemachine2; public class State { private String name; public State(String nm) { name = nm; } public String toString() { return name; } } ///:~
In the state transition diagram, an input
is tested to see if it meets the condition necessary to transfer to the state
under question. As before, the Input is just a tagging
interface:
//: c04:statemachine2:Input.java // Inputs to a state machine package c04.statemachine2; public interface Input {} ///:~
The
Condition evaluates the Input to decide whether this row in the
table is the correct transition:
//: c04:statemachine2:Condition.java // Condition function object for state machine package c04.statemachine2; public interface Condition { boolean condition(Input i); } ///:~
If the Condition returns
true, then the transition to a new state is made, and as that transition
is made some kind of action occurs (in the previous state machine design, this
was the run( ) method):
//: c04:statemachine2:Transition.java // Transition function object for state machine package c04.statemachine2; public interface Transition { void transition(Input i); } ///:~
With these classes in place, we can set
up a 3-dimensional table where each row completely
describes a state. The first element in the row is the current state, and the
rest of the elements are each a row indicating what the type of the input
can be, the condition that must be satisfied in order for this state change to
be the correct one, the action that happens during transition, and the new state
to move into. Note that the Input object is not just used for its type,
it is also a Messenger object that carries information to the
Condition and Transition objects:
{ {CurrentState}, {Input, Condition(Input), Transition(Input), Next}, {Input, Condition(Input), Transition(Input), Next}, {Input, Condition(Input), Transition(Input), Next}, ... }
//: c04:statemachine2:StateMachine.java // A table-driven state machine package c04.statemachine2; import java.util.*; public class StateMachine { private State state; private Map map = new HashMap(); public StateMachine(State initial) { state = initial; } public void buildTable(Object[][][] table) { for(int i = 0; i < table.length; i++) { Object[][] row = table[i]; Object currentState = row[0][0]; List transitions = new ArrayList(); for(int j = 1; j < row.length; j++) transitions.add(row[j]); map.put(currentState, transitions); } } public void nextState(Input input) { Iterator it=((List)map.get(state)).iterator(); while(it.hasNext()) { Object[] tran = (Object[])it.next(); if(input == tran[0] || input.getClass() == tran[0]) { if(tran[1] != null) { Condition c = (Condition)tran[1]; if(!c.condition(input)) continue; // Failed test } if(tran[2] != null) ((Transition)tran[2]).transition(input); state = (State)tran[3]; return; } } throw new RuntimeException( "Input not supported for current state"); } } ///:~
//: c04:vendingmachine:VendingMachine.java // Demonstrates use of StateMachine.java package c04.vendingmachine; import c04.statemachine2.*; final class VM extends State { private VM(String nm) { super(nm); } public final static VM quiescent = new VM("Quiesecent"), collecting = new VM("Collecting"), selecting = new VM("Selecting"), unavailable = new VM("Unavailable"), wantMore = new VM("Want More?"), noChange = new VM("Use Exact Change Only"), makesChange = new VM("Machine makes change"); } final class HasChange implements Input { private String name; private HasChange(String nm) { name = nm; } public String toString() { return name; } public final static HasChange yes = new HasChange("Has change"), no = new HasChange("Cannot make change"); } class ChangeAvailable extends StateMachine { public ChangeAvailable() { super(VM.makesChange); buildTable(new Object[][][]{ { {VM.makesChange}, // Current state // Input, test, transition, next state: {HasChange.no, null, null, VM.noChange}}, { {VM.noChange}, // Current state // Input, test, transition, next state: {HasChange.yes, null, null, VM.makesChange}}, }); } } final class Money implements Input { private String name; private int value; private Money(String nm, int val) { name = nm; value = val; } public String toString() { return name; } public int getValue() { return value; } public final static Money quarter = new Money("Quarter", 25), dollar = new Money("Dollar", 100); } final class Quit implements Input { private Quit() {} public String toString() { return "Quit"; } public final static Quit quit = new Quit(); } final class FirstDigit implements Input { private String name; private int value; private FirstDigit(String nm, int val) { name = nm; value = val; } public String toString() { return name; } public int getValue() { return value; } public final static FirstDigit A = new FirstDigit("A", 0), B = new FirstDigit("B", 1), C = new FirstDigit("C", 2), D = new FirstDigit("D", 3); } final class SecondDigit implements Input { private String name; private int value; private SecondDigit(String nm, int val) { name = nm; value = val; } public String toString() { return name; } public int getValue() { return value; } public final static SecondDigit one = new SecondDigit("one", 0), two = new SecondDigit("two", 1), three = new SecondDigit("three", 2), four = new SecondDigit("four", 3); } class ItemSlot { int price; int quantity; static int counter = 0; String id = Integer.toString(counter++); public ItemSlot(int prc, int quant) { price = prc; quantity = quant; } public String toString() { return id; } public int getPrice() { return price; } public int getQuantity() { return quantity; } public void decrQuantity() { quantity--; } } public class VendingMachine extends StateMachine{ StateMachine changeAvailable = new ChangeAvailable(); int amount = 0; FirstDigit first = null; ItemSlot[][] items = new ItemSlot[4][4]; Condition notEnough = new Condition() { public boolean condition(Input input) { int i1 = first.getValue(); int i2 = ((SecondDigit)input).getValue(); return items[i1][i2].getPrice() > amount; } }; Condition itemAvailable = new Condition() { public boolean condition(Input input) { int i1 = first.getValue(); int i2 = ((SecondDigit)input).getValue(); return items[i1][i2].getQuantity() > 0; } }; Condition itemNotAvailable = new Condition() { public boolean condition(Input input) { return !itemAvailable.condition(input); //int i1 = first.getValue(); //int i2 = ((SecondDigit)input).getValue(); //return items[i1][i2].getQuantity() == 0; } }; Transition clearSelection = new Transition() { public void transition(Input input) { int i1 = first.getValue(); int i2 = ((SecondDigit)input).getValue(); ItemSlot is = items[i1][i2]; System.out.println( "Clearing selection: item " + is + " costs " + is.getPrice() + " and has quantity " + is.getQuantity()); first = null; } }; Transition dispense = new Transition() { public void transition(Input input) { int i1 = first.getValue(); int i2 = ((SecondDigit)input).getValue(); ItemSlot is = items[i1][i2]; System.out.println("Dispensing item " + is + " costs " + is.getPrice() + " and has quantity " + is.getQuantity()); items[i1][i2].decrQuantity(); System.out.println("New Quantity " + is.getQuantity()); amount -= is.getPrice(); System.out.println("Amount remaining " + amount); } }; Transition showTotal = new Transition() { public void transition(Input input) { amount += ((Money)input).getValue(); System.out.println("Total amount = " + amount); } }; Transition returnChange = new Transition() { public void transition(Input input) { System.out.println("Returning " + amount); amount = 0; } }; Transition showDigit = new Transition() { public void transition(Input input) { first = (FirstDigit)input; System.out.println("First Digit= "+ first); } }; public VendingMachine() { super(VM.quiescent); // Initial state for(int i = 0; i < items.length; i++) for(int j = 0; j < items[i].length; j++) items[i][j] = new ItemSlot((j+1)*25, 5); items[3][0] = new ItemSlot(25, 0); buildTable(new Object[][][]{ { {VM.quiescent}, // Current state // Input, test, transition, next state: {Money.class, null, showTotal, VM.collecting}}, { {VM.collecting}, // Current state // Input, test, transition, next state: {Quit.quit, null, returnChange, VM.quiescent}, {Money.class, null, showTotal, VM.collecting}, {FirstDigit.class, null, showDigit, VM.selecting}}, { {VM.selecting}, // Current state // Input, test, transition, next state: {Quit.quit, null, returnChange, VM.quiescent}, {SecondDigit.class, notEnough, clearSelection, VM.collecting}, {SecondDigit.class, itemNotAvailable, clearSelection, VM.unavailable}, {SecondDigit.class, itemAvailable, dispense, VM.wantMore}}, { {VM.unavailable}, // Current state // Input, test, transition, next state: {Quit.quit, null, returnChange, VM.quiescent}, {FirstDigit.class, null, showDigit, VM.selecting}}, { {VM.wantMore}, // Current state // Input, test, transition, next state: {Quit.quit, null, returnChange, VM.quiescent}, {FirstDigit.class, null, showDigit, VM.selecting}}, }); } } ///:~
//: c04:vendingmachine:VendingMachineTest.java // Demonstrates use of StateMachine.java package c04.vendingmachine; import c04.statemachine2.*; import com.bruceeckel.test.*; public class VendingMachineTest extends UnitTest{ VendingMachine vm = new VendingMachine(); Input[] inputs = { Money.quarter, Money.quarter, Money.dollar, FirstDigit.A, SecondDigit.two, FirstDigit.A, SecondDigit.two, FirstDigit.C, SecondDigit.three, FirstDigit.D, SecondDigit.one, Quit.quit, }; public void test() { for(int i = 0; i < inputs.length; i++) vm.nextState(inputs[i]); } public static void main(String[] args) { new VendingMachineTest().test(); } } ///:~
Another approach, as your state machine
gets bigger, is to use an automation tool whereby you configure a table and let
the tool generate the state machine code for you. This can be created yourself
using a language like Python, but there are also free, open-source tools such as
Libero, at http://www.imatix.com.
[9] No
mice were harmed in the creation of this example.