添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
首发于 IT随笔
C++、OC设计模式:状态模式VS有限状态机(11)

C++、OC设计模式:状态模式VS有限状态机(11)

序言:行为即数据

一:状态模式
网上一些文章对状态模式的理解有三个典型错误:

1:错误认为状态模式是对 if else/swith cace 等硬编码状态的解耦
2:错误认为状态模式 将会减少代码变动 ,如果有新状态添加,新增一个类即可(还拿策略模式来做对比)
3:错误认为状态模式就是 唯一状态管理模式

这三个错误观念只所以会出现,我认为是没有真真理解状态模式的定义:

允许一个对象在其内部状态改变时改变它的行为。

对象看起来似乎修改了它的类。

状态模式主要解决的是当控制一个对象状态的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类中,可以把复杂的判断逻辑简化。
... ...

状态模式的定义只 有一句有且只有一句话是对 的,就是这句:“ 允许一个对象在其内部状态改变时改变它的行为

后面使用模糊的暗示对定义进一步做了诠释,但这就很容易把人引入一个大误区,误以为状态模式就是要解决它后半截所描述的问题,包括网上所罗列的优缺点,纯属于“ 为赋新词强说愁 ”。

严格来讲:“ 允许一个对象在其内部状态改变时改变它的行为 ”,这句话虽然拗口但并不难理解,其根本涵义就是:

数据驱动

这里有人有疑问,为什么不是“ 数据驱动 行为 ”? 我们首先看到的是行为,这里的 行为 怎么就没有了?

是的,绝对不能单独包含“ 行为 ”属性,因为 行为本身就是数据 行为 抽象成数据,才是状态模式的精髓

理解到行为即数据,进而遵守以下三个步骤进行拆分,状态模式就非常自然了:
1:把状态”枚举“,分别变成 单独的状态类
2: 每个状态类只处理和自身有关的方法,即: “行为”作为某个状态类的内置方法属性
3: 方法执行完毕后,执行 状态转移 ,设置下一个状态" Next "设为为最新状态(即:当前状态),而且这个" Next " 必须还是 一个不同的 状态类


状态模式包含以下主要角色:

1:环境类(Context)角色:也称为上下文,它定义了客户端需要的接口,内部维护一个当前状态,并负责具体状态的切换。
2:抽象状态(State)角色:定义一个接口,用以封装环境对象中的特定状态所对应的行为,可以有一个或多个行为。
3:具体状态(Concrete State)角色:实现抽象状态所对应的行为,并且在需要的情况下进行状态切换。

代码演示:

#include <stdio.h>
#include <string>
#include <iostream>
using namespace std;
class Context;//声明
//数据驱动模型:状态基类
class IState
public:
    /*行为方法*/
    virtual void Handle(Context *context) = 0;
    //获得当前的状态
    virtual void GetState() = 0;
//状态管理类
class Context
public:
    //初始化状态
    Context(IState *state)
        m_pState = state;
    //获得当前的状态
    IState* GetState()
        return m_pState;
    //改变当前的状态
    void SetState(IState *state)
        m_pState = state;
    //执行状态类实现的方法
    void Request()
        m_pState->Handle(this);
    IState* m_pState;
//具体的状态子类
class ConcreteStateA:public IState
public:
    void Handle(Context *context);
    void GetState();
//具体的状态子类
class ConcreteStateB:public IState
public:
    void Handle(Context *context);
    void GetState();
//具体的状态子类
class ConcreteStateC:public IState
public:
    void Handle(Context *context);
    void GetState();
void ConcreteStateA:: Handle(Context *context)
    //A->B
    //Next:ConcreteStateB
    context->m_pState = new ConcreteStateB;
    cout<<"当前状态是A,下一状态是B"<<endl;
void ConcreteStateB:: Handle(Context *context)
    //B->C
    //Next:ConcreteStateC
    context->m_pState = new ConcreteStateC;
    cout<<"当前状态是B,下一状态是C"<<endl;
void ConcreteStateC:: Handle(Context *context)
    //C->A
    //Next:ConcreteStateA
    context->m_pState = new ConcreteStateA;
    cout<<"当前状态是C,下一状态是A"<<endl;
void ConcreteStateA:: GetState()
    cout<<"当前状态是A"<<endl;
void ConcreteStateB:: GetState()
    cout<<"当前状态是B"<<endl;
void ConcreteStateC:: GetState()
    cout<<"当前状态是B"<<endl;
int test11_1(int argc, const char * argv[]) {
        //初始化A状态
        Context* context = new Context(new ConcreteStateA);
        context->Request();//由A驱动,Next=B
        context->Request();//由B驱动,Next=C
        context->Request();//由C驱动,Next=A
        getchar();
        return 0;

上述实例展示的是:A->B->C->A 状态转移过程,这是由3个状态组成的环状转移图,非常好懂,也非常简单,用他展示状态模式非常形象。一些简单的好理解的状态图,非常适合状态模式去“解耦”,如:

循环状态:N个元素,N&amp;amp;amp;amp;amp;amp;amp;amp;gt;=2
分叉循环图,N个元素,N最好少于4个



不过遗憾的是, 绝大部分重要业务,状态转移还是会很复杂的 ,其特点是:

1:节点多(状态)

2:边多(转移方程)

如:

这个时候,状态模式也不灵了,传说中的: 如新增一个状态,只需要新增一个子类就能解决的好处,就非常尴尬了

且看下一个主角:FSM!

有限状态机(Finite State Machine):

它是一个有向图形,由一组节点和一组相应的转移函数组成。状态机通过响应一系列事件而“运行”。每个事件都在属于“当前” 节点的转移函数的控制范围内,其中函数的范围是节点的一个子集。函数返回“下一个”(也许是同一个)节点。这些节点中至少有一个必须是终态。当到达终态, 状态机停止。

包含一组状态集(states)、一个起始状态(start state)、一组输入符号集(alphabet)、一个 映射 输入符号和当前状态到下一状态的转换函数(transition function)的计算模型。当输入符号串,模型随即进入起始状态。它要改变到新的状态,依赖于转换函数。在有限状态机中,会有有许多变量,例如,状态 机有很多与动作(actions)转换( Mealy机 )或状态( 摩尔 机)关联的动作,多重起始状态,基于没有输入符号的转换,或者指定符号和状态(非定有 限状态机)的多个转换,指派给接收状态(识别者)的一个或多个状态,等等。
---百度百科

要理解有限状态机,最好的方式不一定是看类似“一句话说清... ..."系列的释义文章,如果没有一定的数学知识储备,就是读上100遍类似文章,也只能似懂非懂。

关于有限状态机的相关数学,推荐几个视频教程:

1:基础课: 北京大学-离散数学(国家级精品课)
2: 形式语言与状态机:有限状态机
3: 有限状态马尔可夫链

课件部分截图:

在ios中,FSM该如何实现呢?
苹果其实在GameplayKit就内置了 GKStateMachine
苹果内置的FSM,继承了状态模式一些特点和优点,就是所有的状态类都要继承 GKState ,并由 每个子类负责状态转移。

由子类负责状态转移,比较适合一些低复杂度状态图,但不适合高复杂度的状态图,个人认为其最大的缺点是:
其状态转移并没有做到真真的数据驱动,还是把最为核心的状态转移写死到代码里了。

有没有比较好的FSM实现?,还真有,我找到一篇比较老的文章,非常有启发性,这是一篇英文技术文章,我的英文其实也是二把刀,但是因为这篇文章太重要了,我还是勉强读下去了,不懂的就google翻译下,不妨碍对文章原理性的理解。
Using State Machines to Handle Workflows
《用有限状态来实现工作流》

这篇文章,大致的思路:
1:第一步设计出一个严谨的状态图,要避免回路,避免死机
2:将状态转换为 状态表 (数据结构:[State : [Event : State]] = [:])
3:FSM维护一个当前状态
4:FSM持有这个状态表,之后由 行为 触发指定的 事件 ,并 更新当前状态

其核心思路就是:将状态图转换成一个数据结构(如:二维表格)

本案例的状态图
//... ...
// 状态:对应的是顶点
//注意:状态是唯一,不容许重复,有多个状态,就有多少个节点 
enum RegistrationStates {
	case Login
	case CreateProfile
	case FacebookProfile
	case SelectAvatar
	case TermsAndConditions
	case Finished
// 行为/事件:对应的是有向边
//注意:事件可以不是唯一的,可以容许重复,有向边的数目一定大于行为数
enum RegistrationActions {
	case Login
	case Next //Next事件就重复了5次
	case Back
	case RegisterLater
	case FacebookRegister
//目的是通过事件机制,将所有分散事件汇总到一个地方处理,子类主要调代理接口而已,而不必实现状态子类,避免了过多的子类化
//这样做的好处是:大大降低了代码量,当然前提你需要集中管理页面流程,如果一定要搞大量的状态子类,也要把状态转移放到环境类(Context)角色中去
protocol RegistrationActionProtocol {
	func notifyStateMachine(source: UIViewController, _ event: RegistrationActions)
//状态机核心
public class SimpleStateMachine<State, Event> where State: Hashable, Event: Hashable    {
    private(set) var currentState: State//当前状态
    //状态图二维表
    //元素和技巧:
    //1:有向边:为事件,也就是行为
    //2:节点:为状态,每个节点都有1到N条有向边
    //技巧:每个状态都要存储其下属的边(N>=1),以及边所指向的下一个状态
    private var states: [State : [Event : State]] = [:]
    // MARK: - Init
    public init(initialState: State) {
        currentState = initialState
    //返回某个状态:{Next边,State状态} 的集合
    //同上,也可以设置每个状态的,N-S集合
    public subscript(state: State) -> [Event : State]? {
        get {
            return states[state]
        set(transitions) {
            states[state] = transitions
    //魔法代码:
    //1:找到当前状态:{Next边,State状态} 的集合
    //2:开始滑向下一条边,即:执行方法,并将对应的状态提出来,设为当前状态
    public subscript(event: Event) -> State? {
        if let transitions = states[currentState] {
            if let nextState = transitions[event] {
                return nextState
        return nil
    //执行状态转移,开启魔法代码!!好的数据结构就是爽!!!!!!!!
    public func transition(event: Event) -> State? {
        if let nextState = self[event] {
            currentState = nextState
            return nextState
        return nil
 //... ...
 //以下是数据录入
      func viewDidLoad() {
        super.viewDidLoad()
        //思路:将状态图转换成二维表,数据格式[State : [Event : State]] = [:]
        //1:Login状态:有2条出边[Next/Login]
        registrationStateMachine[.Login] = [
            .Next : .CreateProfile,
            .Login : .Finished
        //2:CreateProfile状态:有3条出边[Next/FacebookRegister/RegisterLater]
        registrationStateMachine[.CreateProfile] = [
            .Next : .SelectAvatar,
            .FacebookRegister : .FacebookProfile,
            .RegisterLater : .TermsAndConditions
        //3:FacebookProfile状态:有2条出边[Next/RegisterLater]
        registrationStateMachine[.FacebookProfile] = [
            .Next : .SelectAvatar,
            .RegisterLater : .TermsAndConditions
        //4:SelectAvatar状态:有2条出边[Next/Back]
        registrationStateMachine[.SelectAvatar] = [
            .Next : .TermsAndConditions,
            .Back : .CreateProfile
        //5:TermsAndConditions状态:有1条出边[Next]
        registrationStateMachine[.TermsAndConditions] = [
            .Next : .Finished
        //6:Finished状态:有0条出边,即停机状态,可不录入进去
    //... ... 
// 魔法时刻,事件集中处理,执行状态转移
extension ViewController: RegistrationActionProtocol {
    func notifyStateMachine(source: UIViewController, _ event: RegistrationActions) {
        //执行状态转移,获得下一个状态
        if let nextState = registrationStateMachine.transition(event: event) {
            if event == .Back { return } // UIKit handles popping of view controllers automatically when "back" is tapped
            self.navigationController?.setNavigationBarHidden(false, animated: false)
            // Handle the states we care about
            switch nextState {
            case .CreateProfile:
                self.navigationController?.pushViewController(createProfileViewController, animated: true)
            case .FacebookProfile:
                self.navigationController?.pushViewController(facebookProfileViewController, animated: true)
            case .SelectAvatar:
                // We never want to be able to go back to Facebook Profile, so we dismiss it before pushing the next VC
                if source == facebookProfileViewController {
                    self.navigationController?.popViewController(animated: false)
                self.navigationController?.pushViewController(selectAvatarViewController, animated: true)
            case .TermsAndConditions:
                self.navigationController?.pushViewController(termsAndConditionsViewController, animated: true)
            case .Finished:
                print("Logged in!")