StateMachine不是那么有用,因为您通常会想到另一种更简单的方式来执行您正在执行的事情,而不是使用它。 为了进行修改,无论是为了宣讲过时的内容还是我在上一个博客末尾附加的丑陋的“ C”代码,我都认为我将展示StateMachine在将Twitter推文转换为HTML中的用法。

这个场景只是一次,不是虚构的,也不是难以捉摸的,但这是我前几天要做的事情。 在这种情况下,我有一个应用程序刚刚为经过身份验证的Twitter用户下载了大量时间轴推文。 解析了XML(或JSON)并掌握了我需要格式化以进行显示的推文。 问题在于它们是纯文本格式,我需要将它们转换为HTML,并添加锚标记以生成类似于twitter在twitter主页上格式化相同内容时所执行的操作的方式。

仅供参考,可以使用Twitter API通过以下URL检索用户Tweets:

<a href="https://api.twitter.com/1/statuses/user_timeline.xml?include_entities=true&include_rts=true&screen_name=BentleyMotors&count=2" target="new">https://api.twitter.com/1/statuses/user_timeline.xml?include_entities=true&include_rts=true&screen_name=BentleyMotors&count=2</a>

文本标签中返回一条tweet,其外观如下所示:

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

……这需要转换成如下形式:

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>

这意味着代码必须找到每个以“#”或“ @”开头的单词或以“ http://”开头的URL。

该状态机的URL图如下所示:






此实现与下面的GOF图确实不同,在该应用程序中,我将状态与事件/动作分开了。 这具有改善去耦的好处,并且动作可以与多个状态关联。






聚集你的状态

构建任何状态机时,要做的第一件事就是将您的状态收集在一起。 在最初的GOF模式中,状态是抽象类。 但是,为了简化起见,我更喜欢使用更多现代枚举。 该状态机的状态为:

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类完成的,如下所示:

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(...)

/**

 * 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获取与当前状态关联的动作。 然后调用该动作并执行更新当前状态的操作,以准备下一个字节。

AbstractAction类来更紧密地遵循GOF模式,该类使用以下方法处理每个事件:

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

AbstractAction的完整实现是:

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代码显示了此操作的完成方式…

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被链接为OFFRUNNING状态, ReadyAction被链接为READY状态, CaptureTag操作被链接到HASHTAG,NAMETAGURL状态,而HttpCheckAction被链接到HTTPCHECK状态。

CaptureTag操作链接到一个以上的状态。 这很好,因为CaptureTag采用了Strategy模式来即时更改其行为。 因此,我使用一些通用代码执行一个操作,在注入一个策略对象之后,它可以完成三件事。

写作动作

DefaultAction ,这是在没有有趣的事情发生时调用的动作。 这个动作愉快地获取输入字符并将它们放入输出流,同时寻找某些字符或字符/状态组合。 DefaultAction的核心是processByte(...)方法中的switch语句。

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');

 }

}

switch语句正在检查每个字节。 如果该字节是一个空格,则下一个字节可能是一个特殊字符:“#”表示井号标签的开头,“ @”表示名称标签的开头,“ h”表示URL的开头; 因此,如果找到空间,则DefaultAction将返回READY状态,因为可能还有更多工作要做。 如果找不到空格,则它将返回RUNNING状态,该状态告诉StateMachine在读取下一个字节时调用DefaultAction

DefaultAction还会在一行的开头检查特殊字符,作为推文的第一个字符,例如“#”,“ @”或“ h”。

StateMachine对象,该对象从输入流中读取下一个字节。 由于状态现在为READY ,因此对processByte(...)的下一次调用将检索ReadyAction

@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;

 }

ReadyAction的switch语句中,您可以看到它的责任是通过分别检查'#','@'和'h'来确认代码已找到井号,名称或URL。 如果找到一个,则返回以下状态之一: HASHTAGNAMETAGHTTPCHECK到StateMachine

ReadyAction找到了一个'#'字符并返回了HASHTAG状态,则StateMachine在读取下一个字节时,将从stateActionMap中 插入带有注入的HashTagStrategy类的CaptureTag类。

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实现。

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。 在这种情况下,代码必须解压缩一些Base 64 XML字段,并且由于该模式是可重用的,所以我只是从我的代码示例工具箱中将其挖掘出来,用于Tweet to HTML的情况。

3完整项目可在github上找到 ……

参考 : Captain Debug的Blog博客上的JCG合作伙伴 Roger Hughes提供了将状态机模式实现为流处理器 的信息 。

翻译自: https://www.javacodegeeks.com/2012/05/implementing-state-machine-pattern-as.html