状态机设计模式

如何解决状态机设计模式

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. 是否存在针对状态机转换的顺序代码类型的警告?从表面上看,它可能会显示为自定义机器代码的编译代码,但我认为这里的区别在于它是有限状态机而不是代码。

解决方法

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

  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,// 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那样使用),并且每个都提供自己的权衡点

,

我喜欢用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情况的开关,其中许多情况仅转换为错误状态。这些实现实际上是等效的。两者都是完全规则的-固定大小的数组或固定数量的案例。两者都允许添加新的状态或事件,而不会(通常)造成庞大的,无法维护的混乱。当然,两者都需要高度的自律才能以可读的方式实施。

,

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

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

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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


依赖报错 idea导入项目后依赖报错,解决方案:https://blog.csdn.net/weixin_42420249/article/details/81191861 依赖版本报错:更换其他版本 无法下载依赖可参考:https://blog.csdn.net/weixin_42628809/a
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下 2021-12-03 13:33:33.927 ERROR 7228 [ main] o.s.b.d.LoggingFailureAnalysisReporter : *************************** APPL
错误1:gradle项目控制台输出为乱码 # 解决方案:https://blog.csdn.net/weixin_43501566/article/details/112482302 # 在gradle-wrapper.properties 添加以下内容 org.gradle.jvmargs=-Df
错误还原:在查询的过程中,传入的workType为0时,该条件不起作用 <select id="xxx"> SELECT di.id, di.name, di.work_type, di.updated... <where> <if test=&qu
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct redisServer’没有名为‘server_cpulist’的成员 redisSetCpuAffinity(server.server_cpulist); ^ server.c: 在函数‘hasActiveC
解决方案1 1、改项目中.idea/workspace.xml配置文件,增加dynamic.classpath参数 2、搜索PropertiesComponent,添加如下 <property name="dynamic.classpath" value="tru
删除根组件app.vue中的默认代码后报错:Module Error (from ./node_modules/eslint-loader/index.js): 解决方案:关闭ESlint代码检测,在项目根目录创建vue.config.js,在文件中添加 module.exports = { lin
查看spark默认的python版本 [root@master day27]# pyspark /home/software/spark-2.3.4-bin-hadoop2.7/conf/spark-env.sh: line 2: /usr/local/hadoop/bin/hadoop: No s
使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams['font.sans-serif'] = ['SimHei'] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -> systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping("/hires") public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate<String
使用vite构建项目报错 C:\Users\ychen\work>npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-