上篇聊了观察者模式和策略模式。今天来说另一个在机器人开发里特别实用的模式——状态模式。
先讲个面试场景。
面试官说:"假设你做一个服务机器人,它有巡逻、充电、避障、回充这几种行为,你怎么设计这个行为切换的逻辑?"
我当时怎么答的?一个Robot类,里面一堆枚举加if-else:
enum class RobotState { PATROL, CHARGING, AVOIDING, RETURNING }; class Robot { RobotState state_; void update() { if (state_ == RobotState::PATROL) { patrol(); if (batteryLow()) state_ = RobotState::RETURNING; if (obstacleDetected()) state_ = RobotState::AVOIDING; } else if (state_ == RobotState::CHARGING) { charge(); if (batteryFull()) state_ = RobotState::PATROL; } else if (state_ == RobotState::AVOIDING) { avoid(); if (obstacleCleared()) state_ = RobotState::PATROL; } else if (state_ == RobotState::RETURNING) { returnToStation(); if (atStation()) state_ = RobotState::CHARGING; } } };面试官看完说:"如果再加十种状态呢?这个函数会变成什么样?"
确实会崩。这就是状态模式要解决的问题。
状态模式的核心思想
状态模式的思路其实很直觉:把每个状态封装成一个独立的类,状态之间的切换逻辑也放在各自的类里面。主类不再负责具体行为,只维护一个当前状态的引用。
class RobotState { public: virtual void handle(Robot& robot) = 0; virtual ~RobotState() = default; }; class Robot { private: unique_ptr<RobotState> state_; public: void setState(unique_ptr<RobotState> s) { state_ = std::move(s); } void update() { state_->handle(*this); } };每个具体状态是一个类,继承RobotState:
class PatrolState : public RobotState { public: void handle(Robot& robot) override { robot.patrol(); if (robot.batteryLow()) { robot.setState(make_unique<ReturningState>()); } if (robot.obstacleDetected()) { robot.setState(make_unique<AvoidingState>()); } } }; class ChargingState : public RobotState { public: void handle(Robot& robot) override { robot.charge(); if (robot.batteryFull()) { robot.setState(make_unique<PatrolState>()); } } };这样加新状态只需要加一个新类,不用改任何已有代码。开闭原则,还记得吧?
在机器人行为管理中的实际应用
ROS2里有个行为树(Behavior Tree)框架叫BehaviorTree.CPP,做机器人导航的基本都在用。行为树的每个节点,其实就是一种状态。
状态模式和行为树的区别,面试偶尔会聊到。行为树更灵活,可以组合和并行执行;状态模式更简洁,适合状态之间互斥切换的场景。
实际项目里,如果你的机器人行为是"同一时间只做一件事",状态模式就够了。比如一个扫地机器人:清扫、回充、待机、脱困,四个状态来回切。
class CleaningState : public RobotState { PathPlanner& planner_; public: CleaningState(PathPlanner& p) : planner_(p) {} void handle(Robot& robot) override { auto target = planner_.nextTarget(); robot.moveToward(target); if (robot.batteryLevel() < 15) { robot.setState(make_unique<ChargingState>()); } else if (robot.stuckDetected()) { robot.setState(make_unique<DeStuckState>()); } else if (robot.cleaningDone()) { robot.setState(make_unique<IdleState>()); } } };还有一个好处:状态的进入和退出动作可以明确管理。进入巡逻状态时启动激光雷达,退出时关闭——这些逻辑写在状态类的构造和析构里,比在switch-case里到处插flag清晰太多了。
状态模式的几个变体
面试如果被追问"状态模式还有什么花样",可以提两个。
第一个是状态机表驱动法。不用类继承,而是用一张表描述"当前状态+事件→下一状态"的映射。适合状态多但逻辑简单的场景:
using TransitionTable = map<pair<State, Event>, State>; TransitionTable table = { {{PATROL, BATTERY_LOW}, RETURNING}, {{RETURNING, AT_STATION}, CHARGING}, {{CHARGING, BATTERY_FULL}, PATROL}, {{PATROL, OBSTACLE}, AVOIDING}, {{AVOIDING, CLEAR}, PATROL} };第二个是层级状态机(Hierarchical State Machine)。状态可以嵌套,子状态继承父状态的行为。比如"导航中"这个大状态下面有"规划路径"和"跟踪路径"两个子状态。这个在无人机飞控里用得比较多。
面试中的关键考点
面试官问状态模式,几个经典追问:
"状态模式和策略模式有什么区别?"结构上看几乎一样——都是上下文加一个策略/状态的接口。核心区别在于:策略模式中,用户主动选择不同的策略;状态模式中,状态的切换是自动的,由状态内部决定。另外,策略之间通常不互相切换,而状态之间会频繁流转。
"状态模式怎么避免状态对象频繁创建?"如果状态切换很频繁,每次都new一个状态对象确实有开销。解决办法是状态对象做成单例或者预分配——如果状态不携带数据,所有实例都一样,完全可以用一个静态实例。
"状态模式和switch-case比有什么优势?"主要是可维护性。switch-case的问题在于,每加一个状态就要改那个巨大的switch,违反开闭原则。而且状态多了之后,switch里很容易漏掉某些case。状态模式把每个状态的行为局部化了,改一个状态不会影响其他状态。
给正在准备面试的你
状态模式在设计模式面试里属于"中等频率"考点。不像工厂模式和单例那样几乎必考,但在机器人、游戏这些涉及行为管理的领域,面试官很喜欢问。
准备的时候,建议手写一个三到四个状态的状态机,比如交通灯(红→绿→黄→红)、自动售货机(待机→投币→出货→找零)。能写出来,能讲清楚和策略模式的区别,面试就够用了。
下篇聊SOLID原则——面试官问"你怎么设计类的"时的评判标准。这个知识点是面向对象设计的根基和基础,前面聊的所有设计模式,背后都在遵循这些原则。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。 你的支持是我持续更新的最大动力。
「机器人软件开发面试·从入门到精通」连载系列上一篇:第47篇 设计模式之观察者与策略——ROS2回调与行为切换 下一篇预告:第49篇 SOLID原则——面试官问"你怎么设计类的"评判标准
有任何问题欢迎评论区留言,我会尽量回复。