前言
- (题外话)
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 良好代码需要具备的三大特性
- 可读性 (Readability):可读性是指代码能够被其他开发者(或未来的自己)轻松理解和维护。可读的代码清晰地表达了程序的意图,使得开发者能够快速理解代码的目的和工作原理,而不需要大量的时间去理解每一行代码。
- *可读性的具体表现:
- 命名规范: 变量、函数、类和方法的命名应具有描述性,能够直观反映其用途。例如,
calculateTotalAmount()
比calc()
更具描述性。 - 清晰的结构: 代码组织良好,易于导航。例如,使用合理的文件夹结构、模块化设计、合适的注释等。
- 一致性: 项目中的命名、缩进和编码风格保持一致,避免混乱和不必要的复杂性。
- 注释: 适量的注释帮助解释为什么要做某件事,而非如何做。复杂的算法和逻辑应该附有说明,避免过度注释。
- 命名规范: 变量、函数、类和方法的命名应具有描述性,能够直观反映其用途。例如,
- 可维护性 (Maintainability):可维护性是指代码能够随着需求的变化、技术的更新或 bug 的修复而轻松进行修改和扩展。良好的可维护性意味着代码结构清晰,具备良好的组织,使得开发者可以快速定位问题并做出修改。
- *可维护性的具体表现:
- 模块化: 代码被合理分割成模块、类和函数,每个模块完成一个明确的功能,减少了代码的耦合性。
- 高内聚低耦合: 每个模块或函数应该有清晰、单一的职责,模块之间的依赖关系应该尽量减少。
- 易于扩展: 当需求发生变化时,代码能够方便地进行扩展而不会影响已有的功能。遵循开闭原则(Open/Closed Principle),即"对扩展开放,对修改封闭"。
- 避免硬编码: 避免将变化的内容写死在代码中(如配置、路径等),而是通过外部配置文件、环境变量等方式管理。
- 可扩展性 (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 问题分析
- 假设我们有一个电梯控制系统,其中电梯有三种状态:
- 停止(Stopped):电梯处于停止状态,等待指令。
- 上升(MovingUp):电梯在上升中。
- 下降(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
的实现 -
如有错误,欢迎指出!!!
-
感谢大家的支持!!!