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


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

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

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

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

Цели:

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

Наблюдения:

  • Состояния тривиальны - нет информации или функций/данных, только опознавание
  • Не похож на шаблон Состояния!
  • Машина управляет переходами из состояния в состояние
  • 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);
  
}
}
// /:~