1.2 状态机模型
一旦团队达成共识,认为对于指定控制器如何运作而言,状态机是一个恰当的抽象,那么,下一步就是确保这个抽象能够运用到软件自身。如果人们在考虑控制器行为时,也要考虑事件、状态和转换,那么,我们希望这些词汇也可以出现在软件代码里。从本质上说,这就是领域驱动设计(Domain–Driven Design)中的Ubiquitous Language [Evans DDD] 原则,也就是说,我们在领域人员(那些描述建筑安全该如何运作的人)和程序员之间构建的一种共享语言。
对于Java程序来说,处理这种事,自然的方式就是以状态机为Domain Model [Fowler PoEAA]。状态机框架的类图见图1-2。
通过接收事件消息和发送命令消息,控制器得以同设备通信。这些消息都是四字母编码,它们可以通过通信通道进行发送。在控制器代码里,我想用符号名(symbolic name)引用这些消息。我创建了事件类和命令类,它们都有代码(code)和名字(name)。我把它们放到单独的类里(有一个超类),因为在控制器的代码里,它们扮演着不同的角色。
class AbstractEvent...
private String name, code;
public AbstractEvent(String name, String code) {
this.name = name;
this.code = code;
}
public String getCode() { return code;}
public String getName() { return name;}
public class Command extends AbstractEvent
public class Event extends AbstractEvent
状态类记录了它会发送的命令及其相应的转换。
class State...
private String name;
private List<Command> actions = new ArrayList<Command>();
private Map<String, Transition> transitions = new HashMap<String, Transition>();
class State...
public void addTransition(Event event, State targetState) {
assert null != targetState;
transitions.put(event.getCode(), new Transition(this, event, targetState));
}
class Transition...
private final State source, target;
private final Event trigger;
public Transition(State source, Event trigger, State target) {
this.source = source;
this.target = target;
this.trigger = trigger;
}
public State getSource() {return source;}
public State getTarget() {return target;}
public Event getTrigger() {return trigger;}
public String getEventCode() {return trigger.getCode();}
状态机保存了其起始状态。
class StateMachine...
private State start;
public StateMachine(State start) {
this.start = start;
}
这样,从这个状态可以到达状态机里的任何状态。
class StateMachine...
public Collection<State> getStates() {
List<State> result = new ArrayList<State>();
collectStates(result, start);
return result;
}
private void collectStates(Collection<State> result, State s) {
if (result.contains(s)) return;
result.add(s);
for (State next : s.getAllTargets())
collectStates(result, next);
}
class State...
Collection<State> getAllTargets() {
List<State> result = new ArrayList<State>();
for (Transition t : transitions.values()) result.add(t.getTarget());
return result;
}
为了处理重置事件,我在状态机上保存了一个列表。
class StateMachine...
private List<Event> resetEvents = new ArrayList<Event>();
public void addResetEvents(Event... events) {
for (Event e : events) resetEvents.add(e);
}
像这样用一个单独结构处理重置事件并不是必需的。简单地在状态机上声明一些额外的转换,也可以处理这种情况,如下所示:
class StateMachine...
private void addResetEvent_byAddingTransitions(Event e) {
for (State s : getStates())
if (!s.hasTransition(e.getCode())) s.addTransition(e, start);
}
我倾向于在状态机上设置显式的重置事件,这样可以更好地表现意图。虽然这样做确实使状态机有点复杂,但它也更加清晰地表现出通用状态机该如何运作,要定义特定状态机也会更加清晰。
处理完结构,再来看看行为。事实证明,这真的相当简单。控制器有个handle方法,它以从设备接收到的事件代码为参数。
class Controller...
private State currentState;
private StateMachine machine;
public CommandChannel getCommandChannel() {
return commandsChannel;
}
private CommandChannel commandsChannel;
public void handle(String eventCode) {
if (currentState.hasTransition(eventCode))
transitionTo(currentState.targetState(eventCode));
else if (machine.isResetEvent(eventCode))
transitionTo(machine.getStart());
// ignore unknown events
}
private void transitionTo(State target) {
currentState = target;
currentState.executeActions(commandsChannel);
}
class State...
public boolean hasTransition(String eventCode) {
return transitions.containsKey(eventCode);
}
public State targetState(String eventCode) {
return transitions.get(eventCode).getTarget();
}
public void executeActions(CommandChannel commandsChannel) {
for (Command c : actions) commandsChannel.send(c.getCode());
}
class StateMachine...
public boolean isResetEvent(String eventCode) {
return resetEventCodes().contains(eventCode);
}
private List<String> resetEventCodes() {
List<String> result = new ArrayList<String>();
for (Event e : resetEvents) result.add(e.getCode());
return result;
}
对于未在状态上注册的事件,它会直接忽略。对于可识别的任何事件,它就会转换为目标状态,并执行这个目标状态上定义 的命令。