Машина состояний

В то время, как Состояние имеет способы, позволяющие клиентскому программисту изменить реализацию, Машина Состояний заключает в себе структуру для автоматического изменения реализации от одного объекта к другому. Текущая реализация представляет состояние, в котором находится система, а система ведет себя по-разному в зависимости от состояния (поэтому она использует Состояния). В основном, это "машина состояний", использующая объекты.

Код, переводящий систему из одного состояния в другое, часто является Шаблонным Методом, как это видно в следующей рабочей среде для общей машины состояний. Мы начнем с определения интерфейса для входных объектов:

//: statemachine:Input.java
// Входные данняе для машины состояний
package statemachine;
public interface Input {
}
///:~

Каждое состояние может быть запущено (run( )) для выполнения своего поведения, и (в этом дизайне) вы можете также передать ему объект Input, который может сказать состоянию в какое новое состояние нужно перейти. Ключевое различие между этим дизайном и следующими состоит в том, что каждый объект состояния (State) решает, в какое другое состояние он может перейти, основываясь на объекте Input, в то время как для всех последующих дизайнов все переходы между состояниями сводятся в единую таблицу. Другими словами, каждый объект состояния (State) имеет свою собственную небольшую таблицу состояний, а в последующих дизайнах существует единая главная таблица переходов между состояниями для всей системы в целом.

//: statemachine:State.java
// Состояние имеет операции, и может осуществить
// переход в другое Состояние, основываясь на Input:
package statemachine;

public interface State {
  
void run();
  
   State next
(Input i);
} ///:~

StateMachine хранит информацию о текущем состоянии, которое инициализируется конструктором. Метод runALL( ) принимает Iterator для списка объектов Input (здесь использован Iterator для последовательности и простоты; главная особенность состоит в том, что входящая информация поступает откуда угодно). Этот метод не только переводит в следующее состояние, но он также вызывает метод run( ) для каждого объекта состояния - здесь вы можете увидеть раскрытие идеи шаблона Состояния, так как метод run( ) делает различные вещи, в зависимости от состояния, в котором находится система.

//: statemachine:StateMachine.java
// Принимает Iterator объектов Inputs для переходов между Состояниями,
// используя шаблонный метод.
package statemachine;

import java.util.*;

public class StateMachine {
  
private State currentState;
  
  
public StateMachine(State initialState) {
     
currentState = initialState;
      currentState.run
();
  
}
  
  
// :
  
public final void runAll(Iterator inputs) {
     
while (inputs.hasNext()) {
        
Input i = (Input) inputs.next();
         System.out.println
(i);
         currentState = currentState.next
(i);
         currentState.run
();
     
}
   }
}
// /:~

Я трактую runAll( ) как шаблонный метод. Это типично, но, конечно, не требуется - у вас может возникнуть желание перегрузить его, но обычно смена поведений будет происходить в методе run( ) Состояния.

На этом основное рабочее пространство Машины Состояний такого стиля (когда каждое состояние решает, какое будет следующим) закончено. В качестве примера я использую затейливую мышеловку, которая может переключаться между несколькими состояниями в процессе поимки мыши [11]. Классы мыши и информация хранятся в пакете mouse, включая класс, представляющий все возможные движения мыши, которые будут являться входной информацией для машины состояний:

//: statemachine:mouse:MouseAction.java
package statemachine.mouse;

import java.util.*;

import statemachine.*;

public class MouseAction implements Input {
  
private String action;
  
private static List instances = new ArrayList();
  
  
private MouseAction(String a) {
     
action = a;
      instances.add
(this);
  
}
  
  
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 forString(String description) {
     
Iterator it = instances.iterator();
     
while (it.hasNext()) {
        
MouseAction ma = (MouseAction) it.next();
        
if (ma.action.equals(description))
           
return ma;
     
}
     
throw new RuntimeException("not found: " + description);
  
}
  
  
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");
} // /:~

Вы можете заметить, что hashCode( ) и equals( ) были перегружены, так что объект MouseAction может быть использован в качестве ключа в HashMap, но в первой версии мышеловки мы не будем делать этого. Также, каждое возможное движение мыши перечисляется, как статический объект MouseAction.

Для создания тестового кода последовательность движений мыши берется из текстового файла:

// :! statemachine: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
// /:~

Чтобы прочесть этот файл, в общем случае здесь применен инструмент общего назначения, называемый StringList:

//: com:bruceeckel:util:StringList.java
// Инструмент общего назначения для чтения из файла
// текстовых строк в список (List) по одной строке.
package com.bruceeckel.util;

import java.io.*;

import java.util.*;

public class StringList extends ArrayList {
  
public StringList(String textFilePath) {
     
try {
        
BufferedReader inputs = new BufferedReader(new FileReader(
              
textFilePath));
         String line;
        
while ((line = inputs.readLine()) != null)
           
add(line.trim());
     
}
     
catch (IOException e) {
        
throw new RuntimeException(e);
     
}
   }
}
// /:~

Этот StringList просто хранит Объекты (Object) точно так же, как и ArrayList, так что нам нужен адаптер, который переведет String в MouseAction:

//: statemachine:mouse:MouseMoveList.java
// "Трансформер" для производства
// списка объектов MouseAction.
package statemachine.mouse;

import java.util.*;

import com.bruceeckel.util.*;

public class MouseMoveList extends ArrayList {
  
public MouseMoveList(Iterator it) {
     
while (it.hasNext())
        
add(MouseAction.forString((String) it.next()));
  
}
}
// /:~

MouseMoveList выглядит почти как декоратор, а работает почти как адаптер. Однако, адаптер меняет один интерфейс на другой, а декоратор добавляет функциональность или данные. MouseMoveList меняет содержимое контейнера, так что его можно представить, как Трансформер.

Создав все инструменты теперь возможно создать первую версию программы мышеловки. Каждый подкласс Состояния определяет свое поведение run( ), а также устанавливает следующее состояние с помощью выражения if-else:

//: statemachine:mousetrap1:MouseTrapTest.java
// Шаблон машины состояний, использующий выражения 'if'
// для определения следующего состояния.
package statemachine.mousetrap1;

import statemachine.mouse.*;

import statemachine.*;

import com.bruceeckel.util.*;

import java.util.*;

import java.io.*;

import junit.framework.*;

// :
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); //
  
}
}

public class MouseTrapTest extends TestCase {
  
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[]) {
     
junit.textui.TestRunner.run(MouseTrapTest.class);
  
}
}
// /:~

Класс StateMachine просто определяет все возможные состояния в виде статических объектов, а также устанавливает начальное состояние. UnitTest создает MouseTrap, а затем проверяет его, получая исходные данные от MouseMoveList.

Пока класс использует выражения if-else внутри метода next( ) это конечно оправдано, но управление большим количеством возможностей становиться затруднительным. Другой подход состоит в создании таблицы внутри каждого объекта State, определяющую различные поведения next( ), основываясь на входящем параметре.

На первый взгляд это выглядит достаточно просто. Вы должны быть способны определить статический метод в каждом подклассе State. Однако, с другой стороны, этот подход генерирует циклически инициализируемые зависимости. Для решения этой проблемы я решил задержать инициализацию таблицы модулей в первый раз до вызова следующего метода next( ) соответствующего State объекта. На первый взгляд метод next( ) по этой причине может показаться немного странным.

Класс StateT является реализацией State (так что может использоваться тот же самый класс StateMachine из предыдущего примера), в который добавлен Map и метод для инициализации карты из двумерного массива. Метод next( ) имеет реализацию в базовом классе, которая должна вызываться из перегруженного наследуемого метода next( ) после того, как он проверит Карту (Map) на null (и проинициализирует ее, если она null):

//: statemachine:mousetrap2:MouseTrap2Test.java
// Улучшенная мышеловка с использованием таблиц
package statemachine.mousetrap2;

import statemachine.mouse.*;

import statemachine.*;

import java.util.*;

import java.io.*;

import com.bruceeckel.util.*;

import junit.framework.*;

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) {
     
// :
     
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 TestCase {
  
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[]) {
     
junit.textui.TestRunner.run(MouseTrap2Test.class);
  
}
}
// /:~

Остальной код идентичен - различия содержаться в методах next( ) и в классе StateT.

Если у вас есть необходимость создать огромное количество классов Состояния, этот подход удобнее, так как он легче для быстрого чтения и понимания переходов между состояниями посредством просмотра таблицы.

Упражнение

  1. Измените MouseTrap2Test.java таким образом, чтобы информация о таблице состояний загружалась из внешнего файла, который содержит только информацию таблицы состояний.

Машина Состояний, управляемая таблицей

Преимущества предыдущего дизайна состоит в том, что вся информация о состоянии, включая информацию о переходе, расположена внутри самого класса состояния. Это, в общем, хороший принцип дизайна.

Однако, в чистых машинах состояния машина может быть полностью представлена единой таблицей переходов между состояниями. В этом заключается преимущество нахождения всей информации о машине состояний в едином месте, что означает, что вы можете легко создавать и поддерживать таблицу, основываясь на диаграмме переходов между состояниями.

Классическая диаграмма переходов между состояниями использует круги для представления каждого состояния и линии, идущие от состояния, указывающие на все состояния, в которое данное состояние может перейти. Каждая линия перехода снабжается примечанием с условием перехода и действием при переходе. Вот как она выглядит:

(Простейшая Диаграмма Машины Состояний)

Цели:

  • Прямая трансляция диаграммы состояния
  • Вектор изменений: представления диаграммы состояний
  • Обоснованная реализация
  • Отсутствие избыточных состояний (вы можете представить каждое изменение в новое состояние)
  • Простота и гибкость

Наблюдения:

  • Состояния тривиальны - нет информации или функций/данных, только опознавание
  • Не похож на шаблон Состояния!
  • Машина управляет переходами из состояния в состояние
  • Similar to flyweight (Схожесть с легким весом)
  • Каждое состояние может переходить во многие другие.
  • Условия и функции действий должны быть внешними по отношению к состояниям
  • Централизованное описание в единой таблице, содержащей все вариации для каждой конфигурации

Пример:

  • Машина состояний и код управляющей таблицы
  • Реализация подающей машины
  • Использование некоторых других шаблонов
  • Отделения общего кода машины состояний от специфического приложения (как в шаблонном методе)
  • Каждая внешняя причина ищет соответствующего решения (объекты, хранящие функции)
  • Ограничения Java: методы являются не первоклассными объектами

Класс состояния

Класс State четко отличается от предыдущего, так как это на самом деле только вместилище с именем. Поэтому он не наследуется от предыдущего класса State:

//: statemachine2:State.java
package statemachine2;

public class State {
  
private String name;
  
  
public State(String nm) {
     
name = nm;
  
}
  
  
public String toString() {
     
return name;
  
}
}
// /:~

Условие перехода

В диаграмме переходов между состояниями входной объект проверяется на то, содержится ли в нем условие, необходимое для перехода в другое состояние. Как и прежде Input это просто соединительный интерфейс:

//: statemachine2:Input.java
// Входной параметр для машины состояний
package statemachine2;

public interface Input {
}
// /:~

Вычисляется Условие (Condition) по полученному Input, чтобы решить, есть ли соответствующая строка в таблице для корректного перехода:

//: statemachine2:Condition.java
// Функциональный объект Condition для машины состояний
package statemachine2;

public interface Condition {
  
boolean condition(Input i);
} // /:~

Действие перехода

Если Условие (Condition) возвращает true, то выполняется переход в новое состояние, а также при переходе выполняются некоторые действия (в предыдущем дизайне машины состояний это выполнялось посредством метода run( )):

//: statemachine2:Transition.java
// Функционнный обеъект перехода для машины состояний
package statemachine2;

public interface Transition {
  
void transition(Input i);
} // /:~

Таблица

С этими классами мы можем построить трехмерную таблицу, в которой каждая строка полностью описывает состояние. Первый элемент строки - текущее состояние, остальные элементы каждой строки указывают возможный тип входящего объекта, условие, которому должен удовлетворять объект, для смены текущего состояния корректным образом, действие, которое будет выполнено при переходе и новое состояние, в которое осуществится переход. Обратите внимание, что объект Input используется не только по своему типу, это также и объект-Сообщение, который переносит информацию об объектах Условия и Перехода:

{ {CurrentState},
 
{Input, Condition(Input), Transition(Input), Next},
 
{Input, Condition(Input), Transition(Input), Next},
 
{Input, Condition(Input), Transition(Input), Next},
  ...
}

Основная машина

//: statemachine2:StateMachine.java
// Управляемая таблицей машина состояний
package 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; //
           
}
           
if (tran[2] != null)
               ((
Transition) tran[2]).transition(input);
            state =
(State) tran[3];
           
return;
        
}
      }
     
throw new RuntimeException("Input not supported for current state");
  
}
}
// /:~

Простая подающая машина

//: statemachine:vendingmachine:VendingMachine.java
// Демонстрация использования StateMachine.java
package statemachine.vendingmachine;

import 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);
     
}
   }
;
   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 } }, });
  
}
}
// /:~

Проверка машины

//: statemachine:vendingmachine:VendingMachineTest.java
// Демонстрация использования StateMachine.java
package statemachine.vendingmachine;

import statemachine2.*;

import junit.framework.*;

public class VendingMachineTest extends TestCase {
  
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) {
     
junit.textui.TestRunner.run(VendingMachineTest.class);
  
}
}
// /:~

Инструменты

Другой подход, когда ваша машина состояний становится громоздкой, состоит в использовании автоматизированного инструмента, посредством которого вы конфигурируете таблицу и позволяете инструменту сгенерировать код машины состояний за вас. Вы можете создать его сами, используя языки, аналогичные Python, но есть также бесплатные инструменты с открытыми исходниками, такие как Libero, на http://www.imatix.com.

Код, управляемый таблицей: гибкость конфигурации

Управляемый таблицей код, с использованием анонимных внутренних классов

Смотрите пример ListPerformance.java в TIJ из Главы 9

Также GreenHouse.java

Упражнения

  1. Примените TransitionTable.java к проблеме "Стиральной машины".
  2. Создайте систему Машины Состояний, в которой текущее состояние наряду со входной информацией определяет следующее состояние, в которое перейдет машина. Чтобы сделать это, каждое состояние должно хранить обратную ссылку на прокси-объект (не контроллер состояний), так чтобы оно могло запросить об изменении состояния. Используйте hashMap для создания таблицы состояний, в которой ключами являются Строки имен нового состояния, а значением является объект нового состояния. Внутри каждого подкласса состояния перегрузите метод nextState( ), чтобы он имел свою собственную таблицу переходов. Входящим объектом для nextInput( ) может быть единственное слово, которое приходит из текстового файла, содержащего по одному слову на строку.
  3. Измените предыдущее упражнение таким образом, чтобы машина состояний могла конфигурироваться путем создания/модификации единого многомерного массива.
  4. Измените упражнение "настроение" из предыдущего раздела так, чтобы оно стало машиной состояния, используя StateMashine.java.
  5. Создайте систему машины состояний лифта, используя StateMashine.java.
  6. Создайте машину сплит-систему, используя StateMashine.java
  7. Генератор - это объект, который производит другие объекты, аналогично фабрике, за исключением того, что для функционирования генератора не требуется никаких аргументов. Создайте MouseMoveGenerator, который производит корректные объекты MouseMove при каждом вызове метода генерации (то есть, мышь должна двигаться в правильной последовательности, при этом возможные движения мыши основываются на предыдущем движении - это другая машина состояний). Добавьте метод iterator( ) для производства итератора, но этот метод должен принимать целый (int) аргумент, чтобы указать число движений, которые необходимо произвести прежде чем hasNext( ) вернет false.