下面是我们编程时常见的一个例子
enum Network_State {
Net_Close,
Net_Open,
Net_Connect,
};
void operation(int state) {
if (state == Net_Close) {
// 。。。。
} else if (state == Net_Open) {
// 。。。。
} else if (state == Net_Connect) {
}
}
这种代码结构特别特别常见,根据当前状态执行不同的操作,符合大多数人直接的编程习惯。
如果这时需要在Network_State中添加新的状态Net_Wait,我们一般的做法也是在operation中修改代码,添加一个分支处理。
以设计模式的角度讲,这违背了开闭原则(对扩展开放,对更改封闭;类模块应该是可扩展的,但是不可修改的)!
,因为它在需求变更时,需要不断地修改内部源代码,而好的设计,应该是以添加对象或接口的方式实现扩展。
如果觉得用设计模式描述问题太深奥,那么我们换点接地气的方式,先就if-else代码结构,慢慢展开分析问题。
if-else结构的问题
使用if-else,或switch-case的结构,一般来说没有很大的问题,简单直接。但有一个比较麻烦的地方在于,当分支情况变得复杂,而且代码时常会面临需求变化时,这时我们再使用if-else会有以下几个问题:
- 分支增加,状态变化更加复杂,我们无法面面俱到,可能会遗漏部分情况;
- 需求变化,某处代码改动会影响到其它部分,代码之间的耦合性太强,复用率低,维护难度大。
对于第一个问题,我们常用的解决方法是使用状态机,画状态迁移图和状态转移表,然后根据状态迁移图进行编程。对于第二个问题,我们应用状态模式,对代码结构进行优化解耦,将状态迁移内化隐藏,消除代码中的if-else。
有限状态机(FSM)
有限状态机可以将复杂的逻辑简化为有限个稳定状态,在稳定状态中判断事件。
左图是一个文件的状态迁移图。右图是其相应的状态转换表,其中第一行蓝色部分是稳定的状态,而第一列黄色部分是可能发生的事件。
有限状态机的设计,可以使得复杂的逻辑代码更加清晰严谨,更加容易维护。
状态模式
在软件构建过程中,某些对象的状态如果改变,其行为也会随之而发生变化,比如文档处于只读状态,其运行的行为和读写状态支持的行为就可能不同。
如何在运行时,根据对象的状态来透明地更改对象的行为?而不会为对象操作和状态转化之间引入紧耦合?(这句话没读懂也不打紧,我们先继续往下走。)
状态模式要点:
State模式将所有与一个特定状态相关的行为都放入一个state的子类对象中,在对象状态切换时,切换相应的对象,但同时维持state接口,这样实现了具体操作与状态转换之间的解耦。
为不同的状态引入不同的对象使得状态转换变得更加明确,而且可以保证不会出现状态不一致的情况,因为转换是原子性的 — 即要么彻底转换,要么不转换。
状态机结合状态模式实例
设计一个简易的MP3播放器,要求两个功能按键,如按下space键切换播放/暂停,按下Esc键停止播放。
简单应用状态机进行编程
代码中有大量的switch-case和if-else。
void onEvent(EventCode ec)
{
switch (state)
{
case ST_PLAY:
if(EV_STOP == ec)
stopPlayer();
else if(EV_PLAY_PAUSE == ec)
pausePlayer();
break;
case ST_PAUSE:
if(EV_STOP == ec)
stopPlayer();
else if(EV_PLAY_PAUSE == ec)
resumePlayer();
break;
default:
break;
}
应用状态模式优化代码结构
#include "stdio.h"
/*
* 有限状态机,结合状态模式实例:一个简易的mp3播放器框架
* 支持两个功能按键,如esc键停止播放,space键切换播放/暂停
*/
// 1、定义状态接口
typedef struct _state { // 某状态下
void (* stop) (); // 停止键的处理函数
void (* play_or_pause) (); // 播放/暂停键的处理函数
} mp3_state;
// 2. 定义系统的当前状态指针
mp3_state *p_cur_state;
// 3. 定义具体的状态,根据状态迁移图来具体实现功能和状态切换
void play_null();
void play_start();
void play_pause();
void play_resume();
void play_stop();
// 初始化态,不能stop,可以play
mp3_state IDLE = {
play_null, // 等价于.stop = play_null,
play_start,
};
mp3_state PLAY = {
play_stop,
play_pause,
};
mp3_state PAUSE = {
play_stop,
play_resume,
};
void play_null() {
// 空函数
}
void play_start() {
printf("开始播放音乐\n");
// ....... // 执行操作
p_cur_state = &PLAY; // 切换状态
}
void play_stop() {
printf("停止播放音乐\n");
p_cur_state = &IDLE;
}
void play_pause() {
printf("暂停播放音乐\n");
p_cur_state = &PAUSE;
}
void play_resume() {
printf("恢复播放音乐\n");
p_cur_state = &PLAY;
}
// 4. 定义上下文操作接口,主程序只关心当前状态,不关心状态之间是如何变化的
void onStop(void) {
p_cur_state->stop();
}
void onPlayOrPause(void) {
p_cur_state->play_or_pause();
}
// 5. 指定系统的起始状态
void mp3player_init() {
p_cur_state = &IDLE;
}
// 主程序通过上下文操作接口来控制系统状态变化
int main(int argc, char const* argv[])
{
mp3player_init(); // 当前为IDLE态
mp3_state context = { // 用context隔离内部p_cur_state的迁移
onStop,
onPlayOrPause,
};
// cur_state next_state
context.play_or_pause(); // IDEL态, 进入播放
context.play_or_pause(); // PLAY态, 暂停播放
context.play_or_pause(); // PAUSE态,恢复播放
context.stop(); // PLAY态, 停止播放
return 0;
}
需求变更时,如何扩展添加一个新状态?
创建一个新的状态对象,然后实现其状态下对应按键的处理函数,最后将函数实现赋给对象内部的函数指针。