这篇解决一个问题设备软件里很多模块的流程骨架一样启动→运行→停止→回零→结批但每一步的具体实现不同。怎么写才不让骨架代码复制粘贴也不让子类从头写起。从最直觉的写法开始一步步讲清模板方法模式解决什么。一、先从一个生活例子说起你开一家奶茶店做奶茶的流程是固定的烧水。放茶叶。泡茶。加奶。加糖。封杯。这个流程是「祖传秘方」每个店员都要按这个步骤来不能乱序。但每一步的具体做法可以不同红茶用 100 度水绿茶用 80 度全脂奶还是脱脂奶加糖还是不加糖。老板的做法是把流程步骤写死在墙上骨架但每一步用什么茶叶、什么奶、什么糖让店员自己定细节。店员不用从头设计流程只要按墙上的步骤填自己的材料就行。这就是模板方法。模板方法的本质父类定义流程骨架先做什么再做什么子类填每一步的具体实现。骨架不变细节可变。二、回到代码不用模板方法会怎样设备软件里每个 Actor 都有生命周期启动、运行、停止、回零、结批。流程骨架一样但每个模块的具体操作不同。最直觉的写法每个子类从头写class Arm { public: void Run() { m_bRunFlag true; Log(手臂启动); CreateThread(); } void Stop() { m_bRunFlag false; Log(手臂停止); } void Home() { m_bHomeFlag true; Log(手臂回零); CreateHomeThread(); } void EndLot() { m_bEndingLot true; Log(手臂结批); } void WorkFlow() { /* 手臂特有的状态机 */ } void HomeFlow() { /* 手臂特有的回零流程 */ } }; class Channel { public: void Run() { m_bRunFlag true; Log(料道启动); CreateThread(); } void Stop() { m_bRunFlag false; Log(料道停止); } void Home() { m_bHomeFlag true; Log(料道回零); CreateHomeThread(); } void EndLot() { m_bEndingLot true; Log(料道结批); } void WorkFlow() { /* 料道特有的状态机 */ } void HomeFlow() { /* 料道特有的回零流程 */ } }; class Buffer { // ... 又是几乎一样的 Run/Stop/Home/EndLot };问题一堆问题一重复代码。Run、Stop、Home、EndLot的逻辑在每个类里几乎一样只是Log里的名字不同。十个模块就是十份重复。问题二改骨架要改所有类。 有一天Run里要加「检查安全门」十个类都要加。漏一个就出 bug。问题三流程顺序没保障。 每个类自己写Run有的先CreateThread再设标志有的反过来。顺序不一致行为可能不同。问题四新人不知道流程该包含什么。 新写一个模块要参考老模块抄一遍容易漏步骤。根本问题流程骨架和具体实现搅在一起没有分离。三、模板方法模式怎么解决思路把流程骨架提到基类写成普通函数非虚函数骨架里调虚函数钩子子类只填虚函数的实现。第一步基类定义骨架class Actor { bool m_bRunFlag false; bool m_bHomeFlag false; bool m_bEndingLot false; bool m_bThreadExist false; public: // 骨架固定流程不可被子类覆盖 void Run() { if (!m_bRunFlag) { m_bRunFlag true; Log(Run); OnRun(); // 钩子子类可以填 CreateThread(); } } void Stop() { if (m_bRunFlag || m_bHomeFlag) { Log(Stop); m_bRunFlag false; m_bHomeFlag false; OnStop(); // 钩子子类可以填 } } void Home() { if (!m_bHomeFlag) { m_bHomeFlag true; Log(Home); OnHome(); // 钩子子类可以填 CreateHomeThread(); } } void EndLot() { if (!m_bEndingLot) { m_bEndingLot true; Log(EndLot); OnEndLot(); // 钩子子类可以填 } } // 钩子子类按需填不填就用默认空实现 protected: virtual void OnRun() {} // 启动时的额外操作 virtual void OnStop() {} // 停止时的额外操作 virtual void OnHome() {} // 回零前的额外操作 virtual void OnEndLot() {} // 结批时的额外操作 // 纯虚钩子子类必须实现 public: virtual void WorkFlow() 0; // 工作流程每个模块必须写 virtual void HomeFlow() 0; // 回零流程每个模块必须写 virtual bool HomeData() 0; // 数据复位每个模块必须写 };第二步子类只填细节class Arm : public Actor { protected: void OnRun() override { Log(手臂额外启动检查); CheckSafeDoor(); } void OnStop() override { Log(手臂额外停止操作); ArmZToSafe(); // 手臂停止时抬 Z } public: void WorkFlow() override { // 手臂特有的状态机 while (true) { Sleep(1); if (!IsRun()) continue; switch (m_step) { ... } } } void HomeFlow() override { // 手臂特有的回零流程 HomeZ(); HomeX(); } bool HomeData() override { m_step.Reset(); m_state.Reset(); return true; } };class Channel : public Actor { protected: void OnStop() override { Log(料道额外停止操作); CloseDoor(); // 料道停止时关门 } public: void WorkFlow() override { // 料道特有的状态机 } void HomeFlow() override { // 料道特有的回零流程 } bool HomeData() override { ResetTrayCount(); return true; } };第三步调用方只调骨架// 总控一键启动 Actor::RunAllActors(); // 内部调每个 Actor 的 Run() // Run() 是骨架自动走完流程子类的 OnRun() 被调 // 总控一键停止 Actor::StopAllActors(); // 内部调每个 Actor 的 Stop() // Stop() 是骨架自动走完流程子类的 OnStop() 被调关键变化Run/Stop/Home/EndLot的骨架只在基类写一次。子类只填OnRun/OnStop/WorkFlow/HomeFlow等钩子。加新模块只填钩子不抄骨架。改骨架比如Run里加安全检查只改基类所有子类自动生效。四、模板方法模式的标准结构角色例子AbstractClass抽象类Actor定义骨架方法和钩子ConcreteClass具体类Arm/Channel/Buffer填钩子实现Template Method模板方法Run()/Stop()/Home()骨架固定不可覆盖Hook钩子OnRun()/OnStop()子类按需填AbstractClassActor │ Run() { 设标志 → OnRun() → CreateThread() } 骨架固定 │ ├──→ ConcreteClassArm │ OnRun() { 检查安全门 } │ WorkFlow() { 手臂状态机 } │ └──→ ConcreteClassChannel OnRun() {} 不需要额外操作空实现 WorkFlow() { 料道状态机 }核心骨架是「非虚函数」子类不能覆盖。钩子是「虚函数」子类按需覆盖。这保证了流程顺序固定细节可变。五、模板方法模式解决了什么问题不用模板方法用模板方法重复代码每个子类抄一遍骨架骨架在基类写一次改骨架改所有子类只改基类流程顺序每个子类自己定可能不一致基类固定保证一致新人上手参考老模块抄容易漏继承基类填钩子不漏强制实现靠人记得写 WorkFlow纯虚函数不写编译不过一句话模板方法把「流程骨架」和「步骤细节」分开骨架不变、细节可变。六、设备软件里的真实场景场景一Actor 生命周期——最典型的模板方法前面讲的Run/Stop/Home/EndLot就是真实场景。每个 Actor 的生命周期骨架一样// 骨架基类 void Run() { 设运行标志; Log(Run); OnRun(); // 钩子子类填额外启动操作 CreateThread(); // 创建线程跑 WorkFlow } // 子类填钩子 void Arm::OnRun() { CheckSafeDoor(); } void Channel::OnRun() { } // 料道不需要额外操作场景二回零流程回零的骨架是固定的回零前准备 → 各轴回零 → 等待完成 → 回零后处理。// 骨架 void HomeFlow() { Log(开始回零); if (!HomeData()) // 钩子1数据复位 { Alarm(回零数据复位失败); return; } DoHome(); // 钩子2各轴回零 WaitHomeComplete(); // 骨架等所有轴回零完成 OnHomeDone(); // 钩子3回零后处理 Log(回零完成); } // 手臂填 void Arm::DoHome() { HomeZ(); HomeX(); } void Arm::OnHomeDone() { ArmToSafe(); } // 料道填 void Channel::DoHome() { HomeConveyor(); } void Channel::OnHomeDone() { CloseDoor(); }骨架保证回零顺序固定先复位→再回零→再等→再后处理每个模块只填「回零哪些轴」「回零后干什么」。场景三取放动作手臂的取料和放料也有固定骨架// 取料骨架 RunRetArmError PickStation(Station* st, Pos pickPos, Pos stPos) { // 1. 移动到站点上方 auto ret MotionToStation(st, pickPos, stPos, ArmMotion::Pick); if (!ret.IsOK()) return ret; // 2. 真空检测钩子不同手臂可能不同 auto airRet CheckArmPickerAir(); if (!airRet.IsOk()) return airRet; // 3. 更新站点信息钩子不同模式更新不同 UpdateInfoFromStation(st, pickPos, stPos); // 4. UI 刷新 UpdateUI(Normal); return airRet; }CheckArmPickerAir和UpdateInfoFromStation可以是虚函数BIB 手臂可能覆盖CheckArmPickerAir加叠料检测Sorting 手臂可能覆盖UpdateInfoFromStation加 Bin 信息。骨架不变细节可变。场景四结批流程结批的骨架也是固定的标记结批 → 等当前任务完成 → 通知下游 → 设结批完成。// 骨架 void EndLot() { m_bEndingLot true; Log(开始结批); OnEndLot(); // 钩子子类填额外结批操作 } // 等结批完成 bool WaitEndLot() { while (!m_bEndedLot) { Sleep(100); if (!IsExistThread()) return false; } OnEndLotDone(); // 钩子子类填结批完成后的清理 return true; }七、模板方法 vs 策略别搞混这两个都涉及「骨架固定、细节可变」容易混。模板方法策略模式谁定骨架父类定骨架Context 不定骨架策略定整个算法怎么变细节子类覆盖钩子换策略对象关系继承is-a组合has-a粒度流程内部的每一步整个算法适合生命周期流程固定整套算法可替换例子Run/Stop/Home 骨架TransportStrategy 整套选站算法简单记模板方法是「骨架不变、填空题」策略是「整张卷子换一份」。模板方法用继承策略用组合。八、模板方法的坑坑一钩子太多子类不知道该填哪个class Actor { protected: virtual void OnRun() {} virtual void OnRunBefore() {} virtual void OnRunAfter() {} virtual void OnRunCheck() {} virtual void OnRunPrepare() {} // ... 十个钩子 };子类一看这么多钩子不知道哪些必须填、哪些可选填。对策钩子要少而精。只有「确实需要子类定制」的地方才开钩子。不需要定制的步骤直接在骨架里写死。坑二子类覆盖了骨架方法class Arm : public Actor { public: void Run() override // 覆盖了骨架 { // 自己写一套基类骨架被绕过 } };如果骨架方法是虚函数子类可能不小心覆盖它导致流程顺序被破坏。对策骨架方法设为非虚函数不加 virtual或者加final关键字禁止覆盖。void Run() final // 子类不能覆盖 { // 骨架 }坑三骨架里调子类钩子时机不对void Run() { CreateThread(); // 先创建了线程 OnRun(); // 再调钩子——但线程已经开始跑了钩子来不及 }钩子的调用时机要放在「正确的步骤之间」。放在线程创建之后子类的OnRun可能来不及生效。对策仔细设计骨架里钩子的位置。OnRun放在设标志之后、创建线程之前。坑四骨架太长变成巨型函数void Run() { // 30 行骨架逻辑 // 5 个钩子调用 // 又 30 行 }骨架太长说明流程太复杂不适合一个模板方法管。对策拆成多个小模板方法。Run调PrepareRunDoRunPostRun每个小方法有自己的钩子。九、可复用结论模板方法模式的本质父类定义流程骨架非虚函数子类填每一步细节虚函数钩子。骨架不变细节可变。两个角色AbstractClass骨架钩子、ConcreteClass填钩子。设备软件最典型应用Actor 的Run/Stop/Home/EndLot生命周期、回零流程、取放动作、结批流程。核心价值骨架写一次不重复改骨架只改基类流程顺序有保证新模块只填钩子不漏步骤。和策略的区别模板方法用继承填钩子骨架不变策略用组合换对象整个算法换。坑钩子要少而精、骨架方法加 final 防覆盖、钩子调用时机要正确、骨架太长就拆。模板方法模式在设备软件里不是「课本上的 Template Method」而是「所有 Actor 的生命周期怎么统一管」的实打实解法。用对了加新模块只填几个钩子骨架自动跑完用错了要么每个子类抄一遍骨架改不动要么钩子太多子类不知道填哪个。关键是分清「骨架该固定的」和「细节该开放的」在正确的地方开钩子在其余地方锁死。