Если вы когда-либо оказывались в ситуации использования FSM в своем проекте, вы, вероятно, начали писать классы для каждого состояния, в котором реализован один и тот же интерфейс. Хороший дизайн мог бы быть:
1
2
3
4
|
interface State { } class State_1 implements State {} class State_2 implements State {} ... |
И у вас, вероятно, был класс, который управлял этими состояниями и переходами между ними, и другой, который реализовывал Контекст FSM (полоса входных полос) и другой интерфейс для начальных состояний и еще один для конечных состояний и так далее. Множество классов разбросано по множеству файлов, что позволяет быстро их потерять.
Есть еще один способ: использование перечислений.
Перечисления по сути представляют собой список классов, и каждый член перечисления может иметь различную реализацию. Предположим, у нас есть следующий конечный автомат, который мы хотим реализовать:
Init — (‘a’) -> A
Init — (остальное) -> Fail
A — (‘b’) -> B
A — (‘c’) -> C
A — (‘a’) -> A
A — (”) -> Конец
A — (остальное) -> Fail
B — (‘c’) -> C
B — (‘b’) -> B
B — (”) -> Конец
B — (остальное) -> Fail
C — (‘c’) -> C
C — (”) -> Конец
C — (остальное) -> Fail
Этот FSM проверит следующее регулярное выражение: ^ (a +) (b *) (c *) $. Мы можем записать состояния как элементы перечисления State следующим образом:
1
2
3
|
interface State { public State next(); } |
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
class Input { private String input; private int current; public Input(String input) { this .input = input;} char read() { return input.charAt(current++); } } enum States implements State { Init { @Override public State next(Input word) { switch (word.read()) { case 'a' : return A; default : return Fail; } } }, A { @Override public State next(Input word) { switch (word.read()) { case 'a' : return A; case 'b' : return B; case 'c' : return C; case '' : return null ; default : return Fail; } } }, B { @Override public State next(Input word) { switch (word.read()) { case 'b' : return B; case 'c' : return C; case '' : return null ; default : return Fail; } } }, C { @Override public State next(Input word) { switch (word.read()) { case 'c' : return C; case '' : return null ; default : return Fail; } } }, Fail { @Override public State next(Input word) { return Fail; } }; public abstract State next(Input word); } |
То, что мы сделали, мы определили переходы каждого состояния внутри каждого перечисления. Каждый переход возвращает новое состояние, и поэтому мы можем циклически проходить через них:
1
2
3
4
5
6
|
State s; Input in = new Input( "aabbbc" ); for (s = States.Init; s != null || s != States.Fail; s = s.next(in)) {} if (s == null ) {System.out.printlin( "Valid!" );} else {System.out.println( "Failed" );} |
На данный момент мы либо проверили строку, либо не удалось. Это простой и элегантный дизайн.
Мы могли бы дополнительно улучшить реализацию, отделив конечные состояния от основных, чтобы упростить условие выхода из обхода. Мы определим другой интерфейс с именем FinalState и второе перечисление, которое будет содержать желаемые состояния выхода (соответственно изменяя перечисление States):
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
interface FinalState extends State {} enum FinalStates implements FinalState { Fail { @Override public State next(Input word) { return Fail; } }, Ok { @Override public State next(Input word) { return End; } } } |
Таким образом, обход будет несколько упрощен:
1
2
3
4
5
6
|
for (s = States.Init; !(s instanceof FinalState); s = s.next(in)) {} switch (s) { case Fail: System.out.printlin( "Failed" ); break ; case Ok: System.out.println( "Valid!" ); break ; default : System.out.println( "Undefined" ); break ; } |
Преимущество состоит в том, что (кроме более простого обхода) мы можем указать более двух конечных состояний. В большинстве случаев FSM будет иметь более одной точки выхода, поэтому это последнее улучшение рекомендуется на длительный срок. Вы также можете поместить все эти перечисления и интерфейсы в один и тот же исходный файл и располагать всю логику в одном месте, а не просматривать несколько вкладок, чтобы понять, как работает поток.
В заключение, использование перечислений делает более компактную и содержательную реализацию FSM. У вас есть вся логика в одном файле, и все обходы безболезненны. У вас также будет гораздо более простой опыт отладки, так как имена состояний, в которые вы перевели, будут отображаться в отладчике (переменная s изменит свое значение соответствующим образом, имея имя нового состояния), скорее всего придется выяснить в какой класс вы только что прыгнули. В общем, я думаю, что это хорошая техника, чтобы иметь в виду.
Ссылка: Автоматизация реализации на Java от нашего партнера JCG Аттилы-Михали Балаза в блоге JUG в Трансильвании .