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

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

//: 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 таким образом, чтобы информация о таблице состояний загружалась из внешнего файла, который содержит только информацию таблицы состояний.