【C++决策和状态管理】从状态模式,有限状态机,行为树到决策树(一):从电梯出发的状态模式State Pattern

前言

  • (题外话)nav2系列教材,yolov11部署,系统迁移教程我会放到年后一起更新,最近年末手头事情多,还请大家多多谅解。
  • 回顾我们整个学习历程,我们已经学习了很多C++的代码特性,也学习了很多ROS2的跨进程通讯方式,也学会了很多路径规划的种种算法。那么如何将这些学习到的东西整合在一起,合并在工程中,使我们的机器人可以自主进行多任务执行和状态切换呢?本系列教程我们就来看看工程中最常用的几个AI控制结构:
    • if-else结构(史山)
    • 状态模式State Pattern
    • 有限状态机FSM
    • 行为树BTtree
    • 决策树

1 史山回顾--if-else 分支结构

1-1 介绍(不需要介绍了吧)
  • if-else 分支结构 是一种最基本的控制流语句,用于根据不同的条件执行不同的代码块。
    • 当条件为真时,执行if块中的代码
    • 当条件为假时,执行else块中的代码。
    • 我们也可以使用多个else if来处理多种不同的条件。
  • 语法特点:
cpp 复制代码
if (condition) {
    // 条件为真时执行的代码块
} else if (another_condition) {
    // 第二个条件为真时执行的代码块
} else {
    // 所有条件都不满足时执行的代码块
}
1-2 缺点分析
  • 不适用于复杂决策 :当决策的条件较多时,if-else结构会变得冗长且难以维护。例如,多个else if分支会导致代码变得难以管理,增加出错的概率。
  • 可读性差:当嵌套层数增加时,代码的可读性会下降,尤其在多个条件需要组合的情况下。
  • 性能问题 :如果条件分支很多,if-else结构的执行效率相对较低。每个条件都需要被评估,可能影响性能。
  • 可扩展低 :当系统需要加入新的判断条件时,必须修改现有的if-else结构,这样会导致现有代码变得更加复杂,增加了出错的风险。

1-3 良好代码需要具备的三大特性
  1. 可读性 (Readability):可读性是指代码能够被其他开发者(或未来的自己)轻松理解和维护。可读的代码清晰地表达了程序的意图,使得开发者能够快速理解代码的目的和工作原理,而不需要大量的时间去理解每一行代码。
  • *可读性的具体表现:
    • 命名规范: 变量、函数、类和方法的命名应具有描述性,能够直观反映其用途。例如,calculateTotalAmount()calc() 更具描述性。
    • 清晰的结构: 代码组织良好,易于导航。例如,使用合理的文件夹结构、模块化设计、合适的注释等。
    • 一致性: 项目中的命名、缩进和编码风格保持一致,避免混乱和不必要的复杂性。
    • 注释: 适量的注释帮助解释为什么要做某件事,而非如何做。复杂的算法和逻辑应该附有说明,避免过度注释。

  1. 可维护性 (Maintainability):可维护性是指代码能够随着需求的变化、技术的更新或 bug 的修复而轻松进行修改和扩展。良好的可维护性意味着代码结构清晰,具备良好的组织,使得开发者可以快速定位问题并做出修改。
  • *可维护性的具体表现:
    • 模块化: 代码被合理分割成模块、类和函数,每个模块完成一个明确的功能,减少了代码的耦合性。
    • 高内聚低耦合: 每个模块或函数应该有清晰、单一的职责,模块之间的依赖关系应该尽量减少。
    • 易于扩展: 当需求发生变化时,代码能够方便地进行扩展而不会影响已有的功能。遵循开闭原则(Open/Closed Principle),即"对扩展开放,对修改封闭"。
    • 避免硬编码: 避免将变化的内容写死在代码中(如配置、路径等),而是通过外部配置文件、环境变量等方式管理。

  1. 可扩展性 (Scalability):可扩展性是指代码能够应对未来需求变化,能够在不大规模重构的情况下进行扩展。当系统的功能、用户量、数据量增长时,良好的扩展性保证了系统能够平稳地进行调整和优化。
  • *可扩展性的具体表现:
    • 灵活的架构: 代码设计采用松耦合、清晰的层次结构和抽象层,确保系统可以随着需求的变化进行扩展。
    • 设计模式的应用: 在适当的场景下,使用合适的设计模式(如工厂模式、策略模式等)帮助代码更容易扩展。
    • 性能优化: 在设计时考虑到性能瓶颈,能够支持大规模的数据和请求。例如,避免重复计算、优化数据库查询等。
    • 无缝集成: 可以轻松地与新的技术、第三方库或服务进行集成,支持横向扩展(增加更多服务器)或纵向扩展(增加服务器性能)。

2 状态模式State Pattern

implements implements holds <<interface>> State +handle() +changeState() ConcreteStateA +handle() +changeState() ConcreteStateB +handle() +changeState() Context -State* currentState +setState(State* newState) +request()

2-1 介绍
  • 状态模式(State Pattern)是一种行为型设计模式,它允许一个对象在其内部状态发生改变时改变其行为,看起来就像是改变了对象的类。换句话说,状态模式让对象在不同的状态下表现出不同的行为,而无需通过条件判断或复杂的控制逻辑来管理状态的转变。
2-2 关键概念
  • Context(上下文):维护当前的状态对象,并委托行为的执行给当前状态对象。
  • State(状态) :定义状态行为的接口。每个具体的状态类实现该接口,并实现相应的状态转换逻辑。
  • ConcreteState(具体状态):每个具体的状态类,定义了不同状态下的具体行为。

3 概念补充

3-1 UML(Unified Modeling Language)概述
  • UML(统一建模语言)是一种标准化的建模语言,用于描述软件系统的结构、行为、交互和功能。它提供了一种通用的图形语言,能够帮助开发人员、架构师和分析师在不同层次上描述复杂的软件系统。
  • UML图有多种类型,常见的包括:
    • 类图(Class Diagram):描述系统中类、接口及其关系(继承、关联等)。
    • 对象图(Object Diagram):展示对象在某一时刻的实例化状态。
    • 用例图(Use Case Diagram):描述系统的功能需求及与外部实体(如用户)的交互。
    • 活动图(Activity Diagram):描述操作流程,类似于流程图。
    • 序列图(Sequence Diagram):描述对象间的交互和消息传递顺序。
    • 状态图(State Diagram):描述对象在生命周期中的状态及状态转换。
    • 组件图(Component Diagram):描述软件的组件结构。
    • 部署图(Deployment Diagram):描述系统的物理部署结构。
  • 在代码中,我们通常会使用类图(Class Diagram)来描述系统中类、接口及其关系(继承、关联等)。
  • 比如说我们来看一段C++的基础接口使用的代码
cpp 复制代码
#include <iostream>
#include <cmath>

class Shape {
public:
    virtual double area() const = 0; // 纯虚函数,要求派生类实现
    virtual void display() const = 0; // 显示形状信息
    virtual ~Shape() {}
};

class Circle : public Shape {
private:
    double radius;

public:
    Circle(double r) : radius(r) {}

    double area() const override {
        return M_PI * radius * radius;
    }

    void display() const override {
        std::cout << "Circle with radius: " << radius << std::endl;
    }
};

class Rectangle : public Shape {
private:
    double width, height;

public:
    Rectangle(double w, double h) : width(w), height(h) {}

    double area() const override {
        return width * height;
    }

    void display() const override {
        std::cout << "Rectangle with width: " << width << " and height: " << height << std::endl;
    }
};

int main() {
    Shape* shapes[2];

    shapes[0] = new Circle(5.0);  // 创建一个Circle对象
    shapes[1] = new Rectangle(4.0, 6.0);  // 创建一个Rectangle对象

    for (int i = 0; i < 2; ++i) {
        shapes[i]->display();
        std::cout << "Area: " << shapes[i]->area() << std::endl;
    }

    // 释放内存
    delete shapes[0];
    delete shapes[1];

    return 0;
}
  • 如果使用UML画出上述代码的类图,则有:

Shape +area() +display() Circle -radius : double +area() +display() Rectangle -width : double -height : double +area() +display()


3-2 状态图(State Diagram)
  • 状态图 (也称为状态机图 ,或状态转换图 )是 UML 的一种重要图类型,用于描述对象或系统在生命周期内的状态变化及其状态之间的转换。它专注于对象在不同状态之间的转移,通常与某个特定对象或类的生命周期相关。
  • 状态图通常包括以下元素:
    • 状态(State):表示对象在某个时间点的特定条件或行为。
    • 初始状态(Initial State):表示状态机的起始状态,用一个实心圆表示。
    • 终止状态(Final State):表示状态机的结束状态,用一个带圈的实心圆表示。
    • 转换(Transition):表示从一个状态到另一个状态的路径,通常通过箭头表示,箭头上标注事件或条件。
    • 事件(Event):触发状态转换的条件或行为。
    • 动作(Action):在状态进入、退出或转换时执行的操作。
  • 状态图的常见用途:
    • 表示对象的生命周期:例如,描述订单在不同处理阶段的状态(如:已创建、处理中、已完成、已取消等)。
    • 描述系统的反应:例如,描述一个TCP连接的不同状态(如:等待连接、建立连接、数据传输、关闭连接等)。
    • 建模复杂的工作流和状态转换:如电梯的状态图,描述电梯在不同楼层之间的状态转换。
  • 示例:简单的状态图
    • 假设我们有一个 电梯 (Elevator),它的状态可以是 "停止""上升""下降"。根据按钮按下或者电梯到达目标楼层,它会从一个状态转移到另一个状态。

goUp() goDown() reachedTop() reachedBottom() goDown() goUp() Stopped MovingUp MovingDown


3 代码实现

  • 基本上所有的状态模式的例子都会牵扯到电梯哈哈哈,那么我们也来看这样一个例子:
3-1 问题分析
  • 假设我们有一个电梯控制系统,其中电梯有三种状态:
    1. 停止(Stopped):电梯处于停止状态,等待指令。
    2. 上升(MovingUp):电梯在上升中。
    3. 下降(MovingDown):电梯在下降中。
  • 电梯根据用户输入的楼层进行相应的状态转换。当电梯达到目标楼层时,它会停止。我们通过状态模式来处理不同状态下的行为。
3-2 状态图
  • 我们画出电梯的状态图。电梯的状态图描述了电梯如何在不同状态之间切换。

goUp() goDown() reachedTop() reachedBottom() goDown() goUp() Stopped MovingUp MovingDown

3-3 类图
  • 下面是状态模式的类图,展示了状态类和上下文类之间的关系。

implements implements implements holds <<interface>> ElevatorState +handle() StoppedState +handle() MovingUpState +handle() MovingDownState +handle() Elevator -ElevatorState* currentState +setState(ElevatorState* state) +request()


3-4 基础代码实现(不加状态约束)
  • 我们简单实现以下代码:
cpp 复制代码
#include <iostream>

// 状态接口类
class ElevatorState {
public:
    virtual ~ElevatorState() {}
    virtual void handle() = 0;
};

// 具体状态类:停止
class StoppedState : public ElevatorState {
public:
    void handle() override {
        std::cout << "Elevator is stopped, waiting for input.\n";
    }
};

// 具体状态类:上升
class MovingUpState : public ElevatorState {
public:
    void handle() override {
        std::cout << "Elevator is moving up.\n";
    }
};

// 具体状态类:下降
class MovingDownState : public ElevatorState {
public:
    void handle() override {
        std::cout << "Elevator is moving down.\n";
    }
};

// 上下文类:电梯
class Elevator {
private:
    ElevatorState* currentState;

public:
    Elevator() : currentState(new StoppedState()) {} // 初始状态为停止状态

    // 设置当前状态
    void setState(ElevatorState* state) {
        delete currentState;    // 删除旧的状态对象
        currentState = state;   // 设置新的状态
    }

    // 请求状态的行为
    void request() {
        currentState->handle();
    }

    ~Elevator() {
        delete currentState; // 删除状态对象,防止内存泄漏
    }
};

// 测试程序
int main() {
    Elevator elevator;

    // 初始状态:停止
    std::cout << "Initial state:\n";
    elevator.request();

    // 切换到上升状态
    elevator.setState(new MovingUpState());
    std::cout << "\nAfter changing to MovingUpState:\n";
    elevator.request();

    // 切换到下降状态
    elevator.setState(new MovingDownState());
    std::cout << "\nAfter changing to MovingDownState:\n";
    elevator.request();

    // 切换回停止状态
    elevator.setState(new StoppedState());
    std::cout << "\nAfter changing back to StoppedState:\n";
    elevator.request();

    return 0;
}
  • ElevatorState 是一个抽象类,定义了所有电梯状态必须实现的接口。
    • 其中的 handle() 方法是一个纯虚函数(即没有实现),所有具体状态类都需要提供具体的实现
  • 接下来的三个类分别代表电梯的不同状态,每个具体的状态类都实现了handle()函数
    • StoppedState 表示电梯处于停止状态。
    • MovingUpState 表示电梯正在上升。
    • MovingDownState 表示电梯正在下降。
  • 电梯类(Elevator)作为上下文类来管理当前状态,并根据状态的变化执行相应的行为

  • 通过不断地调用 setState(),电梯的状态发生变化,最后输出相应的状态行为信息。也就是上述状态模式的本质:它允许一个对象在其内部状态发生改变时改变其行为,看起来就像是改变了对象的类。

3-5 加入转移约束条件
  • 聪明的你一定发现了,上述代码并没有约束状态转换,而是使用了setState()来显式进行状态切换,这是为了初学者能一眼看明白状态模式的本质。
  • 那么接下来,我们可以进一步为上述代码加上状态转换的约束

goUp() goDown() reachedTop() reachedBottom() goDown() goUp() Stopped MovingUp MovingDown

  • 首先我们在状态基类中添加所有事件的实现函数
    • 我们让默认的事件函数设置为调用报错,这样当错误的状态转移发生的时候,报错就会发生
cpp 复制代码
#include <iostream>  
#include <stdexcept>  
  
// 状态接口类  
class ElevatorState {  
public:  
    virtual ~ElevatorState() {}  
    virtual void handle() = 0;  
    virtual void goUp() {  
        throw std::logic_error("Invalid operation for this state");  
    }  
    virtual void goDown() {  
        throw std::logic_error("Invalid operation for this state");  
    }  
    virtual void reachedTop() {  
        throw std::logic_error("Invalid operation for this state");  
    }  
    virtual void reachedBottom() {  
        throw std::logic_error("Invalid operation for this state");  
    }  
};
  • 紧接着我们根据每个状态下能执行的事件对应的函数实现,比如:
    • 我们只实现了reachedTop()goDown(),也就是当电梯处于上升状态时候,合法的事件是reachedTop()goDown(),当你输入其他的事件时候,就会调用默认的实现,也就是报错throw std::logic_error("Invalid operation for this state");
cpp 复制代码
// 具体状态类:上升  
class MovingUpState : public ElevatorState {  
public:  
    void handle() override {  
        std::cout << "Elevator is moving up.\n";  
    }  
  
    void reachedTop() override {  
        std::cout << "Elevator reached the top. Stopping.\n";  
    }  
  
    void goDown() override {  
        std::cout << "Switching to moving down state.\n";  
    }  
};
  • 同时在上下文类中,我们实现一些封装好的函数
cpp 复制代码
// 上下文类:电梯  
class Elevator {  
private:  
    ElevatorState* currentState;  
  
public:  
    Elevator() : currentState(new StoppedState()) {
	    request();
    } // 初始状态为停止状态  
  
    // 设置当前状态  
    void setState(ElevatorState* state) {  
        delete currentState;    // 删除旧的状态对象  
        currentState = state;   // 设置新的状态  
    }  
  
    // 请求状态的行为  
    void request() {  
        currentState->handle();  
    }  
  
    // 封装的 goUp 操作,既改变状态,又执行操作  
    void goUp() {  
        currentState->goUp();  
        setState(new MovingUpState());  
        request();  
    }  
  
    // 封装的 goDown 操作,既改变状态,又执行操作  
    void goDown() {  
        currentState->goDown();  
        setState(new MovingDownState());  
        request();  
  
    }  
  
    // 电梯到达顶部  
    void reachedTop() {  
        currentState->reachedTop();  
  
        setState(new StoppedState());  
        request();  
  
    }  
  
    // 电梯到达底部  
    void reachedBottom() {  
        currentState->reachedBottom();  
        setState(new StoppedState());  
        request();  
  
    }  
  
    ~Elevator() {  
        delete currentState; // 删除状态对象,防止内存泄漏  
    }  
};
  • 我们来进行基础的代码测试:
cpp 复制代码
// 测试程序  
int main() {  
    Elevator elevator;  
    elevator.goUp();  
    elevator.goDown();  
    elevator.reachedBottom();  
  
  
    return 0;  
}
  • 上述操作中
    • goUp() :电梯从停止状态切换到上升状态 (MovingUpState)。
    • goDown() :电梯在上升状态时,用户又要求电梯下降,电梯会从上升状态切换到下降状态 (MovingDownState)。
    • reachedBottom():电梯在下降状态下,达到底部,触发停止状态。
  • 程序输出:
3-6 转移约束条件测试
  • 那我们来测试错误的输入:
cpp 复制代码
// 测试程序  
int main() {  
    Elevator elevator;  
    elevator.goUp();  
    elevator.goUp();  
    
    elevator.goDown();  
    elevator.reachedBottom();  
  
  
    return 0;  
}
  • goUp() 后,电梯应该从 停止状态 切换到 上升状态 (MovingUpState)。
  • goUp() : 当前电梯处于 上升状态 (MovingUpState),调用 goUp() 在上升状态下是不允许的,因此应该抛出异常。这是一个非法操作。

3-7 完整代码
cpp 复制代码
#include <iostream>  
#include <stdexcept>  
  
// 状态接口类  
class ElevatorState {  
public:  
    virtual ~ElevatorState() {}  
    virtual void handle() = 0;  
    virtual void goUp() {  
        throw std::logic_error("Invalid operation for this state");  
    }  
    virtual void goDown() {  
        throw std::logic_error("Invalid operation for this state");  
    }  
    virtual void reachedTop() {  
        throw std::logic_error("Invalid operation for this state");  
    }  
    virtual void reachedBottom() {  
        throw std::logic_error("Invalid operation for this state");  
    }  
};  
  
// 具体状态类:停止  
class StoppedState : public ElevatorState {  
public:  
    void handle() override {  
        std::cout << "Elevator is stopped, waiting for input.\n";  
    }  
  
    void goUp() override {  
        std::cout << "Elevator is starting to move up.\n";  
    }  
  
    void goDown() override {  
        std::cout << "Elevator is starting to move down.\n";  
    }  
};  
  
// 具体状态类:上升  
class MovingUpState : public ElevatorState {  
public:  
    void handle() override {  
        std::cout << "Elevator is moving up.\n";  
    }  
  
    void reachedTop() override {  
        std::cout << "Elevator reached the top. Stopping.\n";  
    }  
  
    void goDown() override {  
        std::cout << "Switching to moving down state.\n";  
    }  
};  
  
// 具体状态类:下降  
class MovingDownState : public ElevatorState {  
public:  
    void handle() override {  
        std::cout << "Elevator is moving down.\n";  
    }  
  
    void reachedBottom() override {  
        std::cout << "Elevator reached the bottom. Stopping.\n";  
    }  
  
    void goUp() override {  
        std::cout << "Switching to moving up state.\n";  
    }  
};  
  
// 上下文类:电梯  
class Elevator {  
private:  
    ElevatorState* currentState;  
  
public:  
    Elevator() : currentState(new StoppedState()) {  
  
        request();  
    } // 初始状态为停止状态  
  
    // 设置当前状态  
    void setState(ElevatorState* state) {  
        delete currentState;    // 删除旧的状态对象  
        currentState = state;   // 设置新的状态  
    }  
  
    // 请求状态的行为  
    void request() {  
        currentState->handle();  
    }  
  
    // 封装的 goUp 操作,既改变状态,又执行操作  
    void goUp() {  
        currentState->goUp();  
        setState(new MovingUpState());  
        request();  
    }  
  
    // 封装的 goDown 操作,既改变状态,又执行操作  
    void goDown() {  
        currentState->goDown();  
        setState(new MovingDownState());  
        request();  
  
    }  
  
    // 电梯到达顶部  
    void reachedTop() {  
        currentState->reachedTop();  
  
        setState(new StoppedState());  
        request();  
  
    }  
  
    // 电梯到达底部  
    void reachedBottom() {  
        currentState->reachedBottom();  
        setState(new StoppedState());  
        request();  
  
    }  
  
    ~Elevator() {  
        delete currentState; // 删除状态对象,防止内存泄漏  
    }  
};  
  
// 测试程序  
int main() {  
    Elevator elevator;  
  
  
    elevator.goUp();  
    elevator.goDown();  
    elevator.reachedBottom();  
  
  
    return 0;  
}

4 小结

  • 本节我们学习了如何通过 状态模式(State Pattern) 来有效地管理一个对象的状态转换及其行为,尤其是在复杂的控制系统(如电梯控制系统)中应用。

  • 下一节我们讲讲有限状态机FSM的实现

  • 如有错误,欢迎指出!!!

  • 感谢大家的支持!!!

相关推荐
若亦_Royi3 分钟前
C++ 的大括号的用法合集
开发语言·c++
嘿嘻哈呀3 小时前
使用ID3算法根据信息增益构建决策树
决策树·机器学习·信息增益·id3算法
ragnwang3 小时前
C++ Eigen常见的高级用法 [学习笔记]
c++·笔记·学习
lqqjuly6 小时前
特殊的“Undefined Reference xxx“编译错误
c语言·c++
冰红茶兑滴水7 小时前
云备份项目--工具类编写
linux·c++
刘好念7 小时前
[OpenGL]使用 Compute Shader 实现矩阵点乘
c++·计算机图形学·opengl·glsl
酒鬼猿8 小时前
C++进阶(二)--面向对象--继承
java·开发语言·c++
姚先生978 小时前
LeetCode 209. 长度最小的子数组 (C++实现)
c++·算法·leetcode
小王爱吃月亮糖9 小时前
QT开发【常用控件1】-Layouts & Spacers
开发语言·前端·c++·qt·visual studio
aworkholic9 小时前
opencv sdk for java中提示无stiching模块接口的问题
java·c++·opencv·jni·opencv4android·stiching