boost 状态机--中级篇

原文:The Boost Statechart Library
译者:penghuster

进阶主题:数码相机

目前一切都很好,然而,上述方法也存在以下限制:

  • 可扩展性差:只要编译器到达 state_machine::initiate() 调用所在位置,大量的模板类实例化将发生,这只有在状态机所有的状态都完整申明后才能成功。这也就是说,状态机的所有代码必须在一个单独编译单元中完成(虽然action能够被单独编译,但在这里并不是焦点)。对于更大的状态机而言,这将导致以下限制:
    • 在某种程度上,编译器为了达到内部模板实例化会造成一些限制和舍弃。这通常发生在中等尺寸的状态机上。例如,在调试模式一个通用的编译器拒绝编译任何超过3位的比特机的早期版本。这意味着编译器达到了它的极限在8个状态,24个转变和16个状态,64个转变。
    • 多程序员协同编码同一个状态机是困难的,因为每一点改动都将不可避免地导致整个状态机的重新编译。
  • 对于一个事件而言最多一个动作触发:根据 UML 一个状态可能有多个能够被同一事件触发的动作。这使得动作的互助排外守卫发挥作用。上面的例子中仅仅是一个事件最多触发一个无守卫的动作。而且,UML 概念中转接和选择点不能直接支持此概念。

所有的这些限制可以通过自定义动作来克服。注意:滥用自定义动作很容易导致未定义行为。请在使用自定义动作前学习此文档。

延伸状态机到多转变单元

比如说公司想要开发一款数码相机。相机需要有如下控制功能:

  • 快门按钮,此按钮可以半按和全按。与此相关的事件分别为 EvShutterHalf, EvShutterFull 和 EvShutterReleased。
  • 设置按钮,代表的事件是 EvConfig。
  • 许多此处不关注的其它按钮。

一个相机用例,在任何配置模式下拍照者可以半按快门,并且相机将立即进入拍照模式。下面的状态图表是完成此行为的一种方式:

配置和拍摄状态将包括大量的内嵌状态,而空闲状态相对简单。因此决定组建两个团队。一个团队实现拍摄模式,另一个实现配置模式。两个团队已经就拍摄团队用于获取配置设置的接口达成一致。我们想要确保两个团队在最少可能接口下工作。我们放两个状态到状态转变单元中,如此机器在配置状态的改变将不会导致拍摄状态下内部工作的重编。反之亦然。

不像之前的样例,这里的代码摘录部分表示同样效果的不同方式,这也导致了下面摘录代码不同于实际执行的样例中代码。注释中对于此类代码进行了标记。
camera.hpp

#ifndef CAMERA_HPP_INCLUDED
#define CAMERA_HPP_INCLUDED

#include <boost/statechart/event.hpp>
#include <boost/statechart/state_machine.hpp>
#include <boost/statechart/simple_state.hpp>
#include <boost/statechart/custom_reaction.hpp>

namespace sc = boost::statechart;

struct EvShutterHalf : sc::event< EvShutterHalf > {};
struct EvShutterFull : sc::event< EvShutterFull > {};
struct EvShutterRelease : sc::event< EvShutterRelease > {};
struct EvConfig : sc::event< EvConfig > {};

struct NotShooting;
struct Camera : sc::state_machine< Camera, NotShooting >
{
  bool IsMemoryAvailable() const { return true; }
  bool IsBatteryLow() const { return false; }
};

struct Idle;
struct NotShooting : sc::simple_state<
  NotShooting, Camera, Idle >
{
  // 对于订制动作,我们仅仅指定我们可能对于一个对应事件的动作,但是这个实际动作
//是被定义在动作成员函数,此成员函数将在 .cpp 中实现。
  typedef sc::custom_reaction< EvShutterHalf > reactions;

  sc::result react( const EvShutterHalf & );
};

struct Idle : sc::simple_state< Idle, NotShooting >
{
  typedef sc::custom_reaction< EvConfig > reactions;

  // ...
  sc::result react( const EvConfig & );
};

#endif

camera.cpp

#include "Camera.hpp"

// 下面的头文件是仅仅在此处而不会出现在camera.hpp中,拍摄和配置状态可以使用相同的模式来
//隐藏其内部实现。这能够确保两个团队互不妨碍的相互协同工作。
#include "Configuring.hpp"
#include "Shooting.hpp"

// not part of the Camera example
sc::result NotShooting::react( const EvShutterHalf & )
{
  return transit< Shooting >();
}

sc::result Idle::react( const EvConfig & )
{
  return transit< Configuring >();
}

注意:任何调用 simple_state<>::transit<>() 和simple_state<>::terminate() (参见引用)将不可避免的析构状态对象(类似于delete this)。也就是说,此代码执行后再对此调用将会导致未定义错误。这也是为何这些函数应该仅仅被作为返回状态的一部分被调用。

延迟事件

拍摄状态的内部工作流程如下:

当使用者半按快门时,将进入拍摄状态和其内部初始化状态聚焦状态。进入聚焦状态,触发相机命令焦圈对拍摄主体进行对焦。然后焦圈根据柔性焦距透镜组进行移动,并在对焦完成后立即发送 EvInFocus 事件。当然,在柔性焦距透镜组还在移动的过程中,使用者能全按快门。在没有任何预警的情况下,由于聚焦状态下没有定义此事件的动作,此结果事件 EvShutterFull 将直接丢失。因此,在相机对焦完成后,使用者将不得不再次全按快门。为了避免此问题,在 Focusing 状态中 EvShutterFull 事件将被延迟。这意味着此类型的所有事件是存储在一个独立的队列中,此队列在 Focusing 状态退出时注入主队列中。

struct Focusing : sc::state< Focusing, Shooting >
{
  typedef mpl::list<
    sc::custom_reaction< EvInFocus >,
    sc::deferral< EvShutterFull >
  > reactions;

  Focusing( my_context ctx );
  sc::result react( const EvInFocus & );
};

动作守卫

Focused 的两个状态转变都源于 Focused,被同样但有两个互斥守卫的事件。这有一个合适的自定义动作:

// not part of the Camera example
sc::result Focused::react( const EvShutterFull & )
{
  if ( context< Camera >().IsMemoryAvailable() )
  {
    return transit< Storing >();
  }
  else
  {
    // 下面是一个实际中内部动作和状态转变的一个混合。看后面如何恰当地实现此转换动作
    std::cout << "Cache memory full. Please wait...\n";
    return transit< Focused >();
  }
}

当然,自定义动作可以在状态声明的时候直接实现,这样对于代码阅读来说是更方便的。

下面我们将用一个守卫来阻止一个转变,如果电池太低,则让外部事件对其作出反应。
camera.cpp

// ...
sc::result NotShooting::react( const EvShutterHalf & )
{
  if ( context< Camera >().IsBatteryLow() )
  {
    // 我们自己不能对于事件做出反应,故我们转移该事件到外部状态(这也是一个状态对
    //于未定义事件所应该进行的默认处理)。
    return forward_event();
  }
  else
  {
    return transit< Shooting >();
  }
}
// ...

状态内动作

Focused 状态的自转变也能够作为一个状态内动作进行实现,只要 Focused 没有任何进入或退出的动作,这将有同样的效果。
shooting.cpp

// ...
sc::result Focused::react( const EvShutterFull & )
{
  if ( context< Camera >().IsMemoryAvailable() )
  {
    return transit< Storing >();
  }
  else
  {
    std::cout << "Cache memory full. Please wait...\n";
    // 表明此事件可以被丢弃,因此,次分配算法将停止此事件寻找一个响应动作,
    //并此状态机将保持在 Focused 状态。
    return discard_event();
  }
}
// ...

因为状态内动作是被守卫的,故需要采用一个 custom_reaction<>,对于无守卫的状态内动作 in_state_reaction 应该被用于更好代码可读性。

转变动作

按照每个转变的效果,动作应该按照如下顺序进行执行:

  1. 从最内部的激活状态开始,执行所有的退出动作,直到但不包括最内部公共上下文。
  2. 执行动作转换(如果在位的话)
  3. 从最内部的公共上下文开始,执行所有的入口动作,直到目标状态(且该状态被入口初始化状态所跟随)。
    例如:

这里的顺序是: ~D(), ~C(), ~B(), ~A(), t(), X(), Y(), Z()。这个转换动作 t() 在最内部的公共上下文中执行,因为此时源状态已经被析构,而目标状态还没有构造。

按照 Boost.Statechart,转换动作是公共上下文的一部分。也就是说,在 Focusing 和 Focused 之间的状态转变能够实现如下:
shooting.hpp

// ...
struct Focusing;
struct Shooting : sc::simple_state< Shooting, Camera, Focusing >
{
  typedef sc::transition<
    EvShutterRelease, NotShooting > reactions; 

  // ...
  void DisplayFocused( const EvInFocus & );
};

// ...

// not part of the Camera example
struct Focusing : sc::simple_state< Focusing, Shooting >
{
  typedef sc::transition< EvInFocus, Focused,
    Shooting, &Shooting::DisplayFocused > reactions;
};

或者,下面也是可能的(这里状态机是自服务为一个最外部上下文) :

// not part of the Camera example
struct Camera : sc::state_machine< Camera, NotShooting >
{
  void DisplayFocused( const EvInFocus & );
};
// not part of the Camera example
struct Focusing : sc::simple_state< Focusing, Shooting >
{
  typedef sc::transition< EvInFocus, Focused,
    Camera, &Camera::DisplayFocused > reactions;
};

响应地,转变动作也能被下面自定义动作调用:
Shooting.cpp:

// ...
sc::result Focusing::react( const EvInFocus & evt )
{
  // We have to manually forward evt
  return transit< Focused >( &Shooting::DisplayFocused, evt );
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,509评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,806评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,875评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,441评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,488评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,365评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,190评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,062评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,500评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,706评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,834评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,559评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,167评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,779评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,912评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,958评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,779评论 2 354

推荐阅读更多精彩内容