原文: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
应该被用于更好代码可读性。
转变动作
按照每个转变的效果,动作应该按照如下顺序进行执行:
- 从最内部的激活状态开始,执行所有的退出动作,直到但不包括最内部公共上下文。
- 执行动作转换(如果在位的话)
- 从最内部的公共上下文开始,执行所有的入口动作,直到目标状态(且该状态被入口初始化状态所跟随)。
例如:
这里的顺序是: ~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 );
}
网友评论