Статьи

Реализация шаблона конечного автомата в качестве потокового процессора

В своем последнем блоге я сказал, что действительно думал, что некоторые из паттернов Gang Of Four (GOF) становятся несколько устаревшими, и если не устаревшими, то, конечно, непопулярными. В частности, я сказал, что StateMachine не так полезен, как вы обычно можете придумать другой, более простой способ делать то, что вы делаете, а не использовать его. Чтобы исправить ситуацию, как за проповедь морального износа, так и за отвратительный код «C», который я прикрепил к концу моего последнего блога, я подумал, что продемонстрирую использование StateMachine при преобразовании твитов Twitter в HTML.

Сценарий, на этот раз, не надуман или надуман, а то, что я должен был сделать на днях. В этом сценарии у меня есть приложение, которое только что загрузило кучу твитов временной шкалы для аутентифицированного пользователя Twitter. После анализа XML (или JSON) и получения твитов мне нужно было отформатировать их для отображения. Проблема заключалась в том, что они были в виде обычного текста, и мне нужно было преобразовать их в HTML, добавив при этом якорные теги, чтобы создать нечто похожее на то, что делает твиттер, когда он форматирует то же самое на вашей домашней странице.

Просто для справки, пользовательские твиты могут быть получены с помощью API Twitter через следующий URL:

… Где имя пользователя в данном случае — «BentleyMotors». Если вы укажете форматирование XML в URL, твит возвращается в текстовом теге и будет выглядеть примерно так:

1
Deputy PM Nick Clegg visits #Bentley today to tour Manufacturing facilities. #RegionalGrowthFund http://t.co/kX81aZmY http://t.co/Eet31cCA

… и это нужно было преобразовать в нечто вроде этого:

1
2
3
4
Deputy PM Nick Clegg visits <a href=\"https://twitter.com/#!/search/%23Bentley\">#Bentley</a> today to tour Manufacturing facilities.
<a href=\"https://twitter.com/#!/search/%23RegionalGrowthFund\">#RegionalGrowthFund</a>
<a href=\"http://t.co/kX81aZmY\">t.co/kX81aZmY</a>
<a href=\"http://t.co/Eet31cCA\">t.co/Eet31cCA</a>

Основная идея в решении этой проблемы 1 состоит в том, чтобы использовать конечный автомат, который считывает входной поток за байт за раз, чтобы найти хэштеги, имена пользователей и URL-адреса и преобразовать их в теги привязки HTML. Например, из полного твита выше #Bentley

становится

<a href=\"https://twitter.com/#!/search/%23Bentley\"> #Bentley </a>

и http://t.co/Eet31cCA

становится

<a href=\"http://t.co/Eet31cCA\"> t.co/Eet31cCA </a> .

Означает, что код должен найти каждое слово, начинающееся с «#» или «@», или URL, начинающийся с «http: //».

Диаграмма URL для этого конечного автомата выглядит примерно так:

Эта реализация отличается от диаграммы GOF ниже тем, что для этого приложения я отделил состояние от события / действия. Это дает преимущества улучшенной развязки и того, что действия могут быть связаны с несколькими состояниями.

Собирая ваши штаты

Первое, что нужно сделать при создании любого конечного автомата, это собрать воедино ваши состояния. В исходном паттерне GOF состояния были абстрактными классами; однако я предпочитаю использовать более современные перечисления для простоты. Состояния для этого конечного автомата:

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
public enum TweetState {
 
 
 
 OFF("Off - not yet running"), //
 
 RUNNING("Running - happily processing any old byte bytes"), //
 
 READY("Ready - found a space, so there's maybe soemthing to do, but that depends upon the next byte"), //
 
 HASHTAG("#HashTag has been found - process it"), //
 
 NAMETAG("@Name has been found - process it"), //
 
 HTTPCHECK("Checking for a URL starting with http://"), //
 
 URL("http:// has been found so capture the rest of the URL");
 
 
 
 private final String description;
 
 TweetState(String description) {
 this.description = description;
 }
 
 @Override
 public String toString() {
 return "TweetState: " + description;
 }
 
}

Чтение байтов

Следующая вещь, которая необходима, — это класс, который читает входной поток по байтам за раз, получает класс действия, связанный с текущим состоянием машины, и обрабатывает байт с помощью действия. Это делается классом StateMachine, показанным ниже:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
public class StateMachine<T extends Enum<?>> {
 
 private final byte[] inputBuffer = new byte[32768];
 private T currentState;
 private final Map<T, AbstractAction<T>> stateActionMap = new HashMap<T, AbstractAction<T>>();
 
 public StateMachine(T startState) {
 
 this.currentState = startState;
 
 }
 /**
 
 * Main method that loops around and processes the input stream
 
 */
 
 public void processStream(InputStream in) {
 
 // Outer loop - continually refill the buffer until there's nothing
 // left to read
 
 try {
 processBuffers(in);
 terminate();
 } catch (Exception ioe) {
 throw new StateMachineException("Error processing input stream: "
 + ioe.getMessage(), ioe);
 }
 
 }
 
 private void processBuffers(InputStream in) throws Exception {
 
 for (int len = in.read(inputBuffer); (len != -1); len = in
 .read(inputBuffer)) {
 
 // Inner loop - process the contents of the Buffer
 
 for (int i = 0; i < len; i++) {
 
 processByte(inputBuffer[i]);
 
 }
 
 }
 
 }
 
 /**
 
 * Deal with each individual byte in the buffer
 
 */
 
 private void processByte(byte b) throws Exception {
 // Get the set of actions associated with this state
 
 AbstractAction<T> action = stateActionMap.get(currentState);
 
 // do the action, get the next state
 
 currentState = action.processByte(b, currentState);
 
 }
 
 
 
 /**
 
 * The buffer is empty. Make sue that we tidy up
 
 */
 
 private void terminate() throws Exception {
 
 AbstractAction<T> action = stateActionMap.get(currentState);
 
 action.terminate(currentState);
 
 }
 
 
 /**
 
 * Add an action to the machine and associated state to the machine. A state
 
 * can have more than one action associated with it
 
 */
 
 public void addAction(T state, AbstractAction<T> action) {
 
 stateActionMap.put(state, action);
 
 }
 
 
 
 /**
 
 * Remove an action from the state machine
 
 */
 
 public void removeAction(AbstractAction<T> action) {
 
 stateActionMap.remove(action); // Remove the action - if it's there
 
 }
 
}

Ключевым методом здесь является processByte (...)

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
/**
 
* Deal with each individual byte in the buffer
 
*/
 
private void processByte(byte b) throws Exception {
 
 
 
// Get the set of actions associated with this state
 
AbstractAction<T> action = stateActionMap.get(currentState);
 
// do the action, get the next state
 
currentState = action.processByte(b, currentState);
 
}

Для каждого байта этот метод получает действие, связанное с текущим состоянием из stateActionMap . Затем вызывается действие и выполняется обновление текущего состояния, готового к следующему байту.

Разобрав состояния и конечный автомат, следующий шаг — написать действия. На этом этапе я более точно следую шаблону GOF, создав класс AbstractAction, который обрабатывает каждое событие с помощью…

1
public abstract T processByte(byte b, T currentState) throws Exception;

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

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
public abstract class AbstractAction<T extends Enum<?>> {
 /**
 
 * This is the next action to take - See the Chain of Responsibility Pattern
 
 */
 
 protected final AbstractAction<T> nextAction;
 
 /** Output Stream we're using */
 
 protected final OutputStream os;
 
 /** The output buffer */
 
 protected final byte[] buff = new byte[1];
 
 public AbstractAction(OutputStream os) {
 
 this(null, os);
 
 }
 
 public AbstractAction(AbstractAction<T> nextAction, OutputStream os) {
 
 this.os = os;
 
 this.nextAction = nextAction;
 
 }
 
 /**
 
 * Call the next action in the chain of responsibility
 
 *
 
 * @param b
 
 * The byte to process
 
 * @param state
 
 * The current state of the machine.
 
 */
 
 protected void callNext(byte b, T state) throws Exception {
 
 if (nextAction != null) {
 nextAction.processByte(b, state);
 }
 
 }
 
 /**
 
 * Process a byte using this action
 
 *
 
 * @param b
 
 * The byte to process
 
 * @param currentState
 
 * The current state of the state machine
 
 *
 
 * @return The next state
 
 */
 
 public abstract T processByte(byte b, T currentState) throws Exception;
 
 /**
 
 * Override this to ensure an action tides up after itself and returns to a
 
 * default state. This may involve processing any data that's been captured
 
 *
 
 * This method is called when the input stream terminates
 
 */
 
 public void terminate(T currentState) throws Exception {
 
 // blank
 
 }
 
 protected void writeByte(byte b) throws IOException {
 
 buff[0] = b; // Write the data to the output directory
 
 os.write(buff);
 }
 
 
 
 protected void writeByte(char b) throws IOException {
 
 writeByte((byte) b);
  
}
 
}

Построение государственной машины

До сих пор весь код, который я написал, был универсальным и может быть использован снова и снова 2 , и все это означает, что следующим шагом будет написание некоторого специфичного для домена кода. На приведенной выше диаграмме UML вы можете видеть, что действия, относящиеся к домену: DefaultAction , ReadyAction и CaptureTags . Прежде чем я продолжу описывать, что они делают, вы, возможно, догадались, что некоторые из них мне нужны, чтобы внедрить действия в StateMachine и связать их с TweetState . Код JUnit ниже показывает, как это делается…

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
StateMachine<TweetState> machine = new StateMachine<TweetState>(TweetState.OFF);
// Add some actions to the statemachine
 
// Add the default action
 
machine.addAction(TweetState.OFF, new DefaultAction(bos));
 
machine.addAction(TweetState.RUNNING, new DefaultAction(bos));
 
machine.addAction(TweetState.READY, new ReadyAction(bos));
 
machine.addAction(TweetState.HASHTAG, new CaptureTag(bos, new HashTagStrategy()));
 
machine.addAction(TweetState.NAMETAG, new CaptureTag(bos, new UserNameStrategy()));
 
machine.addAction(TweetState.HTTPCHECK, new CheckHttpAction(bos));
 
machine.addAction(TweetState.URL, new CaptureTag(bos, new UrlStrategy()));

Из приведенного выше кода видно, что DefaultAction связывает состояния OFF и RUNNING , ReadyAction связывается с состоянием READY , действие CaptureTag связано с состояниями HASHTAG, NAMETAG и URL, а действие HttpCheckAction связано с состоянием HTTPCHECK .

Возможно, вы заметили, что действие CaptureTag связано с более чем одним состоянием. Это хорошо, потому что CaptureTag использует шаблон Strategy , чтобы изменить свое поведение на лету; следовательно, у меня есть одно действие с некоторым общим кодом, который после внедрения объекта стратегии может сделать три вещи.

Написание действий

Возвращаясь к написанию действий, первым действием для записи обычно является действие DefaultAction , которое является действием, которое вызывается, когда ничего интересного не происходит. Это действие успешно принимает входные символы и помещает их в выходной поток, одновременно просматривая определенные символы или комбинации символов / состояний. Сердцем DefaultAction является оператор switch в методе processByte (...) .

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public class DefaultAction extends AbstractAction<TweetState> {
 public DefaultAction(OutputStream os) {
 
 super(os);
 
 }
 
 /**
 
 * Process a byte using this action
 
 *
 
 * @param b
 
 * The byte to process
 
 * @param currentState
 
 * The current state of the state machine
 
 */
 
 @Override
 public TweetState processByte(byte b, TweetState currentState) throws Exception {
 
 TweetState retVal = TweetState.RUNNING;
 
 // Switch state if a ' ' char
 
 if (isSpace(b)) {
 
 retVal = TweetState.READY;
 writeByte(b);
 } else if (isHashAtStart(b, currentState)) {
 retVal = TweetState.HASHTAG;
 } else if (isNameAtStart(b, currentState)) {
 retVal = TweetState.NAMETAG;
 } else if (isUrlAtStart(b, currentState)) {
 retVal = TweetState.HTTPCHECK;
 
 } else {
 writeByte(b);
 
}
 
 return retVal;
 
}
 
 private boolean isSpace(byte b) {
 
 return b == ' ';
 
 }
 
 private boolean isHashAtStart(byte b, TweetState currentState) {
 
 return (currentState == TweetState.OFF) && (b == '#');
 
 }
 
 private boolean isNameAtStart(byte b, TweetState currentState) {
 
 return (currentState == TweetState.OFF) && (b == '@');
 
 }
 
 private boolean isUrlAtStart(byte b, TweetState currentState) {
 
 return (currentState == TweetState.OFF) && (b == 'h');
 
 }
 
}

Из приведенного выше кода видно, что оператор центрального переключателя проверяет каждый байт. Если байт является пробелом, то следующий байт может быть специальным символом: «#» для начала хэштега, «@» для начала тега имени и «h» для начала URL-адреса; следовательно, если пробел найден, тогда DefaultAction возвращает состояние READY, поскольку может потребоваться дополнительная работа. Если пробел не найден, он возвращает состояние RUNNING, которое сообщает StateMachine вызвать DefaultAction при чтении следующего байта.

Действие DefaultAction также проверяет наличие специальных символов в начале строки, поскольку первый символ твита может быть «#», «@» или «h».

Теперь управление передано обратно объекту StateMachine , который читает следующий байт из входного потока. Поскольку состояние теперь ГОТОВО , следующий вызов processByte (...) извлекает действие ReadyAction .

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
@Override
public TweetState processByte(byte b, TweetState currentState) throws Exception {
 
TweetState retVal = TweetState.RUNNING;
 
switch (b) {
 
case '#':
retVal = TweetState.HASHTAG;
break;
 
case '@':
retVal = TweetState.NAMETAG;
break;
 
case 'h':
retVal = TweetState.HTTPCHECK;
break;
 
default:
super.writeByte(b);
break;
}
 
return retVal;
 
}

Из оператора Switch ReadyAction вы можете видеть, что его обязанность состоит в том, чтобы подтвердить, что код нашел хештег, имя или URL, проверив ‘#’, ‘@’ и ‘h’ соответственно. Если он находит его, он возвращает одно из следующих состояний: HASHTAG , NAMETAG или HTTPCHECK в StateMachine

Предполагая, что ReadyAction обнаружил символ ‘#’ и возвратил состояние HASHTAG , StateMachine при чтении следующего байта извлечет класс CaptureTag с внедренным классом HashTagStrategy из stateActionMap

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
60
61
62
63
64
65
66
67
68
69
70
71
public class CaptureTag extends AbstractAction<TweetState> {
 
 private final ByteArrayOutputStream tagStream;
 private final byte[] buf;
 private final OutputStrategy output;
 private boolean terminating;
 
 public CaptureTag(OutputStream os, OutputStrategy output) {
 
 super(os);
 tagStream = new ByteArrayOutputStream();
 buf = new byte[1];
 this.output = output;
 }
 
 
 /**
 
 * Process a byte using this action
 * @param b
 * The byte to process
 * @param currentState
 * The current state of the state machine
 */
 
 @Override
 public TweetState processByte(byte b, TweetState currentState)
 throws Exception {
 TweetState retVal = currentState;
 
 if (b == ' ') {
 
 retVal = TweetState.READY; // fix 1
 output.build(tagStream.toString(), os);
 if (!terminating) {
 super.writeByte(' ');
 }
 
 reset();
 
 } else {
 buf[0] = b;
 tagStream.write(buf);
 }
 
 return retVal;
 
 }
 
 /**
 
 * Reset the object ready for processing
 
 */
 
 public void reset() {
  
 terminating = false;
 tagStream.reset();
 
 }
 
 @Override
 public void terminate(TweetState state) throws Exception {
 
 terminating = true;
 processByte((byte) ' ', state);
 
 }
 
}

Идея, лежащая в основе кода CaptureTag, заключается в том, что он захватывает символы, добавляя их в ByteArrayOutputStream, пока не обнаружит пробел или входной буфер не станет пустым. При обнаружении пробела CaptureTag вызывает свой интерфейс OutputStrategy , который в этом случае реализуется HashTagStrategy .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
public class HashTagStrategy implements OutputStrategy {
 /**
 * @see state_machine.tweettohtml.OutputStrategy#build(java.lang.String,
 * java.io.OutputStream)
 */
 
 @Override
 public void build(String tag, OutputStream os) throws IOException {
 
 String url = "<a href=\"https://twitter.com/#!/search/%23" + tag + "\">#" + tag + "</a>";
 os.write(url.getBytes());
 
 }
}

HashTagStrategy создает URL-адрес для поиска в хэштеге и записывает его в выходной поток. После того, как URL был записан в поток, CaptureTag возвращает состояние READY — поскольку пространство обнаружено, и возвращает управление в StateMachine .

StateMachine читает следующий байт, и процесс продолжается.

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

В итоге

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

1 Существует несколько способов решения этой головоломки, некоторые из которых могут быть проще и менее сложными, чем State Machine

2 Эта версия StateMachine была написана в 2006 году для обработки XML. В этом сценарии код должен был разархивировать некоторые XML-поля Base 64, и поскольку шаблон можно было повторно использовать, я просто выкопал его из набора инструментов с примерами кода для случая Tweet to HTML.

3 Полный проект доступен на github

Ссылка : Реализация шаблона конечного автомата в качестве потокового процессора от нашего партнера по JCG Роджера Хьюза в блоге Captain Debug’s Blog .