Skip to content

Commit f862c52

Browse files
committed
Translate State
1 parent 3f75622 commit f862c52

File tree

2 files changed

+375
-2
lines changed

2 files changed

+375
-2
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
- [ ] Chapter16: Iterator。
2727
- [x] Chapter19: Nulll Object. 涉及到对代理模式和pimpl编程技法的运用,以及std::optional
2828
- [ ] Chapter20: Observer. 已翻译 属性观察者、模板观察者Observer\<T>、可观察Observable\<T> 、依赖问题和取消订阅与线程安全。
29-
- [] Chapter21: State. 补充字符串匹配、例子
29+
- [x] Chapter21: State. 补充字符串匹配、例子
3030
- [ ] Chapter22: Strategy. 翻译了动态策略。静态策略
3131
- [x] Chapter23: Template Method. 模版方法模式和策略模式的异同。
3232
- [x] Chapter24: Visitor. 入侵式、反射式、经典式的访问者的设计思路,std::visitor在variant类型上的访问。

docs/chapter-21-state.md

Lines changed: 374 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,381 @@ sleepy ----------> alert
2121

2222
#### 状态驱动的状态机
2323

24+
我们将从最简单的例子开始:一个电灯开关。它只能处于开和关的状态。我们将构建一个任何状态都能够切换到其他状态的模型:虽然这反映了状态设计模式的经典实现(根据GoF的书),但我并不推荐这样做。
25+
26+
27+
首先,让我们为电灯开关建模:它只有一种状态和一些从一种状态切换到另一种状态的方法:
28+
29+
```c++
30+
class LightSwitch
31+
{
32+
State* state;
33+
public:
34+
LightSwitch()
35+
{
36+
state = new OffState();
37+
}
38+
void set_state(State* state)
39+
{
40+
this->state = state;
41+
}
42+
};
43+
```
44+
45+
这一切看起来都很合理。我们现在可以定义状态,在这个特定的情况下,它将是一个实际的类:
46+
47+
```c++
48+
struct State
49+
{
50+
virtual void on(LightSwitch* ls)
51+
{
52+
cout << "Light is already on\n";
53+
}
54+
virtual void off(LightSwitch* ls)
55+
{
56+
cout << "Light is already off\n";
57+
}
58+
};
59+
```
60+
61+
这个实现很不直观,所以我们需要慢慢地仔细地讨论它,因为从一开始,关于State类的任何东西都没有意义。
62+
63+
首先,`State`不是抽象的!你会认为一个你没有办法(或理由)达到的状态是抽象的。但事实并非如此。
64+
65+
第二,状态允许从一种状态切换到另一种状态。这对一个通情达理的人来说,毫无意义。想象一下电灯开关:它是改变状态的开关。人们并不指望`State`本身会改变自己,但它似乎就是这样做的。
66+
67+
第三,也许是最令人困惑的,`State::on/off`的默认行为声称我们已经处于这种状态!在我们实现示例的其余部分时,这一点将在某种程度上结合在一起。
68+
69+
现在我们实现`On``Off`状态:
70+
71+
```c++
72+
struct OnState : State
73+
{
74+
OnState() { cout << "Light turned on\n"; }
75+
void off(LightSwitch* ls) override;
76+
};
77+
78+
struct OffState : State
79+
{
80+
OffState() { cout << "Light turned off\n"; }
81+
void on(LightSwitch* ls) override;
82+
};
83+
```
84+
85+
实现OnState::off和OffState::on允许状态本身切换到另一个状态!它看起来是这样的:
86+
87+
```c++
88+
void OnState::off(LightSwitch* ls)
89+
{
90+
cout << "Switching light off...\n";
91+
ls->set_state(new OffState());
92+
delete this;
93+
} // same for OffState::on
94+
```
95+
96+
这就是转换发生的地方。这个实现包含了对`delete This`的奇怪调用,这在真实的c++中是不常见的。这对初始分配状态的位置做出了非常危险的假设。例如,可以使用智能指针重写这个示例,但是使用指针和堆分配清楚地表明状态在这里被积极地销毁。如果状态有析构函数,它将触发,你将在这里执行额外的清理。
97+
98+
当然,我们确实希望开关本身也能切换状态,就像这样:
99+
100+
101+
```c++
102+
class LightSwitch
103+
{
104+
...
105+
void on() { state->on(this); }
106+
void off() { state->off(this); }
107+
};
108+
```
109+
110+
因此,把所有这些放在一起,我们可以运行以下场景:
111+
112+
```c++
113+
1 LightSwitch ls; // Light turned off
114+
2 ls.on(); // Switching light on...
115+
3 // Light turned on
116+
4 ls.off(); // Switching light off...
117+
5 // Light turned off
118+
6 ls.off(); // Light is already off
119+
```
120+
121+
我必须承认:我不喜欢这种方法,因为它不是直观的。当然,状态可以被告知(观察者模式)我们正在进入它。但是,状态转换到另一种状态的想法——根据GoF的书,这是状态模式的经典实现——似乎不是特别令人满意。
122+
123+
如果我们笨拙地说明从`OffState``OnState`的转换,则需要将其说明为
124+
125+
```c++
126+
LightSwitch::on() -> OffState::on()
127+
OffState -------------------------------------> OnState
128+
```
129+
130+
另一方面,从OnState到OnState的转换使用基状态类,这个类告诉你你已经处于那个状态
131+
132+
```c++
133+
LightSwitch::on() -> State::on()
134+
OnState ----------------------------------> OnState
135+
```
136+
137+
这里给出的示例可能看起来特别人为,所以我们现在将看看另一个手工创建的设置,其中的状态和转换被简化为枚举成员。
138+
24139
#### 手工状态机
25140

141+
让我们尝试为一个典型的电话会话定义一个状态机。首先,我们将描述电话的状态:
142+
143+
```c++
144+
enum class State
145+
{
146+
off_hook,
147+
connecting,
148+
connected,
149+
on_hold,
150+
on_hook
151+
};
152+
```
153+
154+
我们现在还可以定义状态之间的转换,也可以定义为`enum class`:
155+
156+
```c++
157+
enum class Trigger
158+
{
159+
call_dialed,
160+
hung_up,
161+
call_connected,
162+
placed_on_hold,
163+
taken_off_hold,
164+
left_message,
165+
stop_using_phone
166+
};
167+
```
168+
169+
现在,这个状态机的确切规则,即可能的转换,需要存储在某个地方。
170+
171+
```c++
172+
map<State, vector<pair<Trigger, State>>> rules;
173+
```
174+
175+
176+
这有点笨拙,但本质上`map`的键是我们移动的状态,值是一组表示`Trigger-State`的对,在此状态下可能的触发器以及使用触发器时所进入的状态。
177+
178+
让我们来初始化这个数据结构:
179+
180+
```c++
181+
rules[State::off_hook] = {
182+
{Trigger::call_dialed, State::connecting},
183+
{Trigger::stop_using_phone, State::on_hook}
184+
};
185+
186+
rules[State::connecting] = {
187+
{Trigger::hung_up, State::off_hook},
188+
{Trigger::call_connected, State::connected}
189+
};
190+
// more rules here
191+
```
192+
193+
我们还需要一个启动状态,如果我们希望状态机在达到该状态后停止执行,我们还可以添加一个退出(终止)状态:
194+
195+
```c++
196+
State currentState { State::off_hook },
197+
exitState { State::on_hook };
198+
```
199+
200+
完成这些之后,我们就不必为实际运行(我们使用`orchestrating`这个术语)状态机而构建单独的组件了。例如,如果我们想要构建电话的交互式模型,我们可以这样做:
201+
202+
```c++
203+
while(true)
204+
{
205+
cout << "The phone is currently " << currentState << endl;
206+
207+
select_trigger:
208+
cout << "Select a trigger:" << "\n";
209+
210+
int i = 0;
211+
for(auto &&item : rules[currentState])
212+
{
213+
cout << i++ << ". " << item.first << "\n";
214+
}
215+
216+
int input;
217+
cin >> input;
218+
for(input < 0 || (input+1) > rules[currentState].size())
219+
{
220+
goto select_trigger;
221+
}
222+
223+
currentState = rules[currentState][input].second;
224+
if(currentState == exitState) break;
225+
}
226+
```
227+
228+
首先:是的,我确实使用`goto`,这是一个很好的例子,说明在什么地方使用`goto`是合适的(译者注:一般不建议在程序里面使用goto,这样会使得程序的控制流比较混乱)。对于算法本身,这是相当明显的:我们让用户在当前状态上选择一个可用的触发器(`operator<<`状态和触发器都在幕后实现了),并且,如果触发器是有效的,我们通过使用前面创建的规则映射转换到它。
229+
230+
如果我们到达的状态是退出状态,我们就跳出循环。下面是一个与程序交互的示例。
231+
232+
```
233+
1 The phone is currently off the hook
234+
2 Select a trigger:
235+
3 0. call dialed
236+
4 1. putting phone on hook
237+
5 0
238+
6 The phone is currently connecting
239+
7 Select a trigger:
240+
8 0. hung up
241+
9 1. call connected
242+
10 1
243+
11 The phone is currently connected
244+
12 Select a trigger:
245+
13 0. left message
246+
14 1. hung up
247+
15 2. placed on hold
248+
16 2
249+
17 The phone is currently on hold
250+
18 Select a trigger:
251+
19 0. taken off hold
252+
20 1. hung up
253+
21 1
254+
22 The phone is currently off the hook
255+
23 Select a trigger:
256+
24 0. call dialed
257+
25 1. putting phone on hook
258+
26 1
259+
27 We are done using the phone
260+
```
261+
262+
这种手工状态机的主要优点是非常容易理解:状态和转换是普通的枚举类,转换集是在一个简单的`std::map`中定义的,开始和结束状态是简单的变量
263+
264+
26265
#### Boost.MSM 中的状态机
27266

28-
#### 总结
267+
在现实世界中,状态机要复杂得多。有时,你希望在达到某个状态时发生某些操作。在其他时候,你希望转换是有条件的,也就是说,你希望转换只在某些条件存在时发生。
268+
269+
`Boost.MSM (Meta State Machine)`,一个状态机库,是Boost的一部分,你的状态机是一个通过`CRTP`继承自`state_ machine_def`的类:
270+
271+
```c++
272+
struct PhoneStateMachine : state_machine_def<PhoneStateMachine>
273+
{
274+
bool angry{ false };
275+
}
276+
```
277+
278+
我添加了一个`bool`变量来指示调用者是否`angry`(例如,在被搁置时); 我们稍后会用到它。现在,每个状态也可以驻留在状态机中,并且可以从`state`类继承:
279+
280+
```c++
281+
struct OffHook : state<> {};
282+
struct Connecting : state<>
283+
{
284+
template<class Event, class FSM>
285+
void on_entry(Event const& evt, FSM&)
286+
{
287+
cout << "We are connecting..." << endl;
288+
}
289+
// also on_exit
290+
};
291+
// other states omitted
292+
```
293+
294+
如你所见,状态还可以定义在进入或退出特定状态时发生的行为。你也可以定义在转换时执行的行为(而不是当你到达一个状态时):这些也是类,但它们不需要从任何东西继承;相反,它们需要提供具有特定签名的`operator()`:
295+
296+
```c++
297+
struct PhoneBeingDestoryed
298+
{
299+
template<class EVT, class FSM, class SourceState, class TargetState>
300+
void operator()(EVT const&, FSM& SourceState&, TargetState&)
301+
{
302+
cout << "Phone breaks into a million pieces" << endl;
303+
}
304+
};
305+
```
306+
307+
正如你可能已经猜到的那样,这些参数提供了对状态机的引用,以及你将要进入和进入的状态。
308+
309+
最后,我们有守卫条件(`guard condition`):这些条件决定我们是否可以在第一时间使用一个转换。现在,我们的布尔变量`angry`不是`MSM`可用的形式,所以我们需要包装它:
310+
311+
```c++
312+
struct CanDestoryPhone
313+
{
314+
template<class EVT, class FSM, class SourceState, class TargetState>
315+
bool operator()(EVT const&, FSM& fsm, SourceState&, TargetState&)
316+
{
317+
return fsm.angry;
318+
}
319+
};
320+
```
321+
322+
前面的例子创建了一个名为`CanDestroyPhone`的守卫条件,稍后我们可以在定义状态机时使用它。
323+
324+
325+
为了定义状态机规则,`Boost.MSM`使用MPL(元编程库)。具体来说,转换表被定义为`mpl::vector`,每一行依次包含:
326+
327+
- 源状态
328+
- 状态转换
329+
- 目标状态
330+
- 一个要执行的可选操作
331+
- 一个可选守卫条件
332+
333+
因此,有了所有这些,我们可以像下面这样定义一些电话呼叫规则:
334+
335+
```c++
336+
struct transition_table : mpl::vector<
337+
Row<OffHook, CallDialed, Connecting>,
338+
Row<Connecting, CallConnected, Connected>,
339+
Row<Connected, PlacedOnHold, OnHold>,
340+
Row<OnHold, PhoneThrownIntoWall, PhoneDestoryed, PhoneBeingDestoryed, CanDestoryPhone>
341+
>
342+
{};
343+
```
344+
345+
在前面的方法中,与状态不同,`CallDialed`之类的转换是可以在状态机类之外定义的类。它们不必继承自任何基类,而且很容易为空,但它们必须是类型。
346+
347+
`transition_table`的最后一行是最有趣的:它指定我们只能尝试在`CanDestroyPhone`保护条件下销毁电话,并且当电话实际上被销毁时,应该执行`PhoneBeingDestroyed`操作。
348+
349+
现在,我们可以添加更多的东西。首先,我们添加起始条件:因为我们正在使用`Boost.MSM`,起始条件是一个类型定义,而不是一个变量:
350+
351+
```c++
352+
typedef OffHook initial_state;
353+
```
354+
355+
最后,如果没有可能的转换,我们可以定义要发生的操作。它可能发生!比如,你把手机摔坏了,就不能再用了,对吧?
356+
357+
```c++
358+
template <class FSM, class Event>
359+
void no_transition(Event const& e, FSM&, int state)
360+
{
361+
cout << "No transition from state " << state_names[state]
362+
<< " on event " << typeid(e).name() << endl;
363+
}
364+
```
365+
366+
`Boost MSM`将状态机分为前端(我们刚刚写的)和后端(运行它的部分)。使用后端API,我们可以根据前面的状态机定义构造状态机:
367+
368+
```c++
369+
msm::back::state_machine<PhoneStateMachine> phone;
370+
```
371+
372+
现在,假设存在`info()`函数,它只打印我们所处的状态,我们可以尝试`orchestrating`以下场景
373+
374+
```c++
375+
1 info(); // The phone is currently off hook
376+
2 phone.process_event(CallDialed{}); // We are connecting...
377+
3 info(); // The phone is currently connecting
378+
4 phone.process_event(CallConnected{});
379+
5 info(); // The phone is currently connected
380+
6 phone.process_event(PlacedOnHold{});
381+
7 info(); // The phone is currently on hold
382+
8 9
383+
phone.process_event(PhoneThrownIntoWall{});
384+
10 // Phone breaks into a million pieces
385+
11
386+
12 info(); // The phone is currently destroyed
387+
13
388+
14 phone.process_event(CallDialed{});
389+
15 // No transition from state destroyed on event struct CallDialed
390+
```
391+
392+
因此,这就是定义更复杂、具有工业强度的状态机的方式。
393+
394+
#### 总结
395+
396+
首先,这是值得强调的`Boost.MSM`是Boost中两种状态机实现之一,另一种是`Boost.statechart`。我很确定还有很多其他的状态机实现。
397+
398+
其次,状态机的功能远不止这些。例如,许多库支持分层状态机的思想:例如,一个`生病(Sick)`的状态可以包含许多不同的子状态,如`流感(Flu)``水痘(Chickenpox)`。如果你在处于感染流感的状态,你也同时处于生病的状态。
399+
400+
最后,有必要再次强调现代状态机与状态设计模式的原始形式之间的差异。重复api的存在(例如`LightSwitch::on/off vs. State::on/off`)以及自删除的存在在我的书中是明确的代码气味。不要误会我的方法是有效的,但它是不直观的和繁琐的。
401+

0 commit comments

Comments
 (0)