您现在的位置:e-works > 智造书屋 > 书籍列表 > 领域特定语言 > 状态机模型

第一章 第1章 入门例子

第二节 状态机模型

    一旦团队达成共识,认为对于指定控制器如何运作而言,状态机是一个恰当的抽象,那么,下一步就是确保这个抽象能够运用到软件自身。如果人们在考虑控制器行为时,也要考虑事件、状态和转换,那么,我们希望这些词汇也可以出现在软件代码里。从本质上说,这就是领域驱动设计(Domain–Driven Design)中的Ubiquitous Language [Evans DDD] 原则,也就是说,我们在领域人员(那些描述建筑安全该如何运作的人)和程序员之间构建的一种共享语言。
    对于Java程序来说,处理这种事,自然的方式就是以状态机为Domain Model [Fowler PoEAA]。状态机框架的类图见图1-2。

状态机框架的类图

图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;
     }
    对于未在状态上注册的事件,它会直接忽略。对于可识别的任何事件,它就会转换为目标状态,并执行这个目标状态上定义的命令。