《领域特定语言》一1.2 状态机模型

状态机模型设计
本文介绍了一种使用状态机模型来设计控制器的方法。该方法通过定义事件、状态和转换,实现控制器与设备之间的通信。文章详细解释了状态机的类结构和工作流程,包括如何处理重置事件。

1.2 状态机模型

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

通过接收事件消息和发送命令消息,控制器得以同设备通信。这些消息都是四字母编码,它们可以通过通信通道进行发送。在控制器代码里,我想用符号名(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;
 }

对于未在状态上注册的事件,它会直接忽略。对于可识别的任何事件,它就会转换为目标状态,并执行这个目标状态上定义 的命令。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值