状态机设计模式

时间:2020-09-03 16:40:57

标签: design-patterns state state-machine

TLDR:是否有任何已知的设计模式将状态机配置表示为无需使用goto语句的代码?

重构有哪些可能的问题:

{
   a: {
      title: 'Step A',
      onActionXGoTo: 'b',
      onActionYGoTo: 'c',
   },
   b: {
      title: 'Step B',
      onAnyActionGoTo: 'c',
   }
   c: {
      title: 'Step C',
      onActionXGoTo: 'a',
      onActionYGoTo: 'b',
   }
}

如以下示例所示。 buildGraph将返回上面的对象

buildGraph((builder) => {
     builder.while(() => {
        if (builder.initial() {
            builder.setStep({ title: 'Step A'});
        } else {
           if (builder.isActionX()) {
              builder.setStep({ title: 'Step A'});
           }
           if (builder.isActionY()) {
              builder.setStep({ title: 'Step B'});
           }
        }
        
        if (builder.isActionX()) {
           builder.setStep({ title: 'Step B'});
           builder.setStep({ title: 'Step C'});
        }
        if (builder.isActionY()) {
           builder.setStep({ title: 'Step C'});
        }
    });
});

此用例可能没有意义,因为它很难遵循。但是,我们有一个具有数百个连接的状态机,很难维护。

  1. 是否有任何设计模式可以帮助实现这一目标?
  2. 状态机的规范由UX / Product作为goto语句给出。现在,将goto语句转换为顺序代码可能值得付出努力,因为实现新状态机时会增加复杂性。我想念什么吗?
  3. 是否存在针对状态机转换的顺序代码类型的警告?从表面上看,它可能会显示为自定义机器代码的编译代码,但我认为这里的区别在于它是有限状态机而不是代码。

3 个答案:

答案 0 :(得分:3)

这是三个状态机的实现。从理论上讲,每个状态都会处理输入并返回另一个状态(除非它是终止状态)。从技术上讲,您有一个值,分支到某个地址以执行某些副作用,然后设置/返回一个值;您可以决定哪一种最适合您的用例:

  1. while / switch
  2. 状态转换表
  3. 状态模式

while / switch(或任何分支构造)

对于小型状态机简单且可维护。对于大型状态机,这会变得快速而无法控制,并且进行更改并不容易

var states = {
    STATE_A: 1,
    STATE_B: 2,
    STATE_C: 3,
    STATE_ERR: 4,
};

var inputs = {
    ACTION_X: 1,
    ACTION_Y: 2,
};

var state = states.STATE_A;

while (1) {
    var action = getAction();
    
    switch (state) {
    case states.STATE_A:
        if (action === inputs.ACTION_X) {
            state = states.STATE_B;
        } else if (action === inputs.ACTION_Y) {
            state = states.STATE_C;
        } else {
            state = states.STATE_ERR;
        }
        break;

    case states.STATE_B:
        if (isAnyAction(action)) {
            state = states.STATE_C;
        }
        break;

    case states.STATE_C:
        if (action === inputs.ACTION_X) {
            state = states.STATE_A;
        } else if (action === inputs.ACTION_Y) {
            state = states.STATE_B;
        }
        break;

    case states.STATE_ERR:
        throw 'invalid';
        break;
    }
}

状态转换表

易于更改,缩放良好,参考位置,可位图,易于生成。不幸的是,状态输入处理程序与状态本身是分开的,并且大小可能变大

var states = {
    STATE_A: 0,
    STATE_B: 1,
    STATE_C: 2
};

var inputs = {
    ACTION_X: 0,
    ACTION_Y: 1,
};


var transitionTable = [
    // ACTION_X,     // ACTION_Y
    [states.STATE_B, states.STATE_C], // STATE_A
    [states.STATE_C, states.STATE_C], // STATE_B
    [states.STATE_A, states.STATE_B], // STATE_C
];

function transition(state, action) {
    return transitionTable[state][action];
}

var state = states.STATE_A; // starting state

while (1) {
    var action = getAction();

    state = transition(state, action);

    // do something for state
}

状态模式

去中心化(与上表不同),提供封装,可扩展。由于支持多态性需要间接访问,因此速度非常慢。失去参考利益的地方

class StateA {
    handleAction(action) {
        if (action === inputs.ACTION_X) {
            return new StateB();
        } else if (action === inputs.ACTION_Y) {
            return new StateC();
        }
    }
}

class StateB {
    handleAction(action) {
        if (isAnyAction(action)) {
            return new StateC();
        }
    }
}

class StateC {
    handleAction(action) {
        if (action === inputs.ACTION_X) {
            return new StateA();
        } else if (action === inputs.ACTION_Y) {
            return new StateB();
        }
    }
}


var state = new StateA(); // starting state

while (1) {
    var action = getAction();

    state = state.handleAction(action);
}

这些都不使用goto(不像javascript那样使用),并且每个都提供自己的权衡点

答案 1 :(得分:1)

我喜欢用C实现FSM的方法是将整个计算机定义为矩阵。矩阵的行表示状态,而列表示导致状态之间转换的事件。因此,如果有M个状态和N个事件,则矩阵的大小为M * N,尽管大多数单个单元格可能表示状态和事件的无效组合。每个状态和事件都可以由整数或enum表示,分别为0 ... M-1和0 ... N-1。这些数字只是矩阵的索引。矩阵本身可以用C(可能是Java)实现为M * N二维数组。其中一种状态通常表示“完成”。将有一个已定义的“开始”状态,该状态可能为零状态,但不一定如此。

在C中,矩阵的单元格(即数组中的条目)可以是函数指针。每个指针指向一个函数,该函数将指向表示数据模型的某些数据结构的指针作为其参数,并返回一个整数。整数表示处理事件后的系统新状态。将有一些特定的返回值(例如-1)表示错误情况。我在与状态和事件的无效组合相对应的单元格中放置了NULL指针。

随着每个事件的到来,代码在矩阵(数组)中查找指示当前状态和事件的单元格。如果不是NULL(错误),则代码将通过其指针调用该函数。该函数执行适合于特定状态/事件组合的所有操作,然后返回新状态。如果它是错误值或“完成”状态的值,则机器停止。

我认为这种方法不能直接在Java中使用,因为它缺少函数指针。但是,我想您可以使用函数引用或lambda函数达到相同的效果。

对于小型模型,可以将矩阵转换为简单的大型switch语句。实际上,这也适用于大型模型,但是代码很难阅读(实际上,我认为任何 FSM实现在进入数百个转换时都很难阅读)。开关的每种情况都是当前状态的总和乘以状态数组的宽度再加上事件。因此,例如,如果每行中有十个事件,则M * 10 + N的每个可能值都会有一个情况。这些值是编译时已知的常数,因此可以将它们用作case中的情况值。一个开关。

当然,就像数组通常会具有许多表示无效事件的NULL值一样,许多情况下,开关只会转换到错误状态而不会做任何事情。我仍然写出每种情况,只是为了有规律性-开关中总是有M * N种情况。

简而言之,可以将事件和状态的M * N矩阵(其中许多成员为NULL)转换为具有M * N情况的开关,其中许多情况仅转换为错误状态。这些实现实际上是等效的。两者都是完全规则的-固定大小的数组或固定数量的案例。两者都允许添加新的状态或事件,而不会(通常)造成庞大的,无法维护的混乱。当然,两者都需要高度的自律才能以可读的方式实施。

答案 2 :(得分:-1)

恕我直言,您应该将系统编写为数据驱动的,并从配置文件中加载状态和转换。

我最近写了一个关于在gamedev中使用FSM的教程,也许您会发现它很有用:https://www.davideguida.com/blazor-gamedev-part-9-finite-state-machine/

要点很简单:每个状态都在自己的类中,由跟踪实体状态的通用FSM类编排。

相关问题