Преимущества предыдущего дизайна состоит в том, что вся информация о состоянии, включая информацию о переходе, расположена внутри самого класса состояния. Это, в общем, хороший принцип дизайна.
Однако, в чистых машинах состояния машина может быть полностью представлена единой таблицей переходов между состояниями. В этом заключается преимущество нахождения всей информации о машине состояний в едином месте, что означает, что вы можете легко создавать и поддерживать таблицу, основываясь на диаграмме переходов между состояниями.
Классическая диаграмма переходов между состояниями использует круги для представления каждого состояния и линии, идущие от состояния, указывающие на все состояния, в которое данное состояние может перейти. Каждая линия перехода снабжается примечанием с условием перехода и действием при переходе. Вот как она выглядит:
(Простейшая Диаграмма Машины Состояний)
Цели:
- Прямая трансляция диаграммы состояния
- Вектор изменений: представления диаграммы состояний
- Обоснованная реализация
- Отсутствие избыточных состояний (вы можете представить каждое изменение в новое состояние)
- Простота и гибкость
Наблюдения:
- Состояния тривиальны - нет информации или функций/данных, только опознавание
- Не похож на шаблон Состояния!
- Машина управляет переходами из состояния в состояние
- 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);
}
} // /:~
← | Машина состояний | Инструменты | → |