上一篇讲了命令模式。
它解决的问题是:
当一次请求需要被传递、排队、记录、撤销、重试、异步执行或统一调度时,如何把请求封装成对象。
比如远程车控指令、诊断请求、OTA 任务、自动化测试步骤,都可以封装成命令对象。
命令模式的重点是:
让请求变成可以被管理的对象。
这一篇讲模板方法模式。
它解决的问题是:
当一个业务流程整体步骤固定,但某些具体步骤会变化时,应该怎么设计。
车企软件里,这种场景非常多:
- 诊断服务处理流程固定,但不同服务的校验和执行不同
- OTA 升级流程固定,但不同升级包的校验、安装、回滚不同
- 远程控制流程固定,但解锁、开空调、寻车的执行动作不同
- 数据上报流程固定,但不同数据源的采集和格式化不同
- 测试用例执行流程固定,但每个测试步骤不同
- 文件解析流程固定,但不同文件格式的解析细节不同
- 参数配置流程固定,但不同配置项的校验和写入不同
这些场景有一个共同点:
流程骨架是稳定的,变化的是其中某些步骤。
模板方法模式,就是用来处理这种问题的。
一、先从一个车企场景说起
假设我们现在做一个诊断服务处理框架。
一个诊断请求进来后,处理流程通常是固定的:
text
接收请求
↓
检查会话权限
↓
检查安全访问权限
↓
校验请求参数
↓
执行业务逻辑
↓
生成响应
↓
记录日志
不同诊断服务的差异在于:
text
ReadDID:校验 DID 是否支持,然后读取数据
WriteDID:校验 DID 是否可写,然后写入数据
RoutineControl:校验例程 ID,然后启动 / 停止例程
ClearDTC:校验 DTC 分组,然后清除故障码
如果直接写,代码可能会这样:
cpp
void DiagnosticService::Handle(const Request& request) {
if (request.sid == 0x22) {
CheckSession(request);
CheckSecurity(request);
CheckReadDidParam(request);
auto data = ReadDid(request);
BuildReadDidResponse(data);
Log(request);
} else if (request.sid == 0x2E) {
CheckSession(request);
CheckSecurity(request);
CheckWriteDidParam(request);
WriteDid(request);
BuildWriteDidResponse();
Log(request);
} else if (request.sid == 0x31) {
CheckSession(request);
CheckSecurity(request);
CheckRoutineParam(request);
ExecuteRoutine(request);
BuildRoutineResponse();
Log(request);
}
}
这段代码的问题很明显。
很多流程是重复的:
text
检查会话
检查安全权限
校验参数
执行业务
生成响应
记录日志
但每个服务又有自己的差异。
如果继续用 if-else 写,后面会越来越乱。
模板方法模式要做的事情就是:
把固定流程放到父类里,把变化步骤延迟到子类实现。
二、为什么流程代码容易写乱?
很多业务流程不是一上来就复杂。
一开始可能只是:
cpp
Check();
Execute();
后来慢慢加需求:
text
执行前要校验权限
执行前要检查车辆状态
执行时要记录耗时
失败时要生成错误码
执行后要上报日志
某些场景要跳过安全校验
某些服务要增加额外前置检查
某些服务失败后要回滚
于是代码就会变成:
cpp
if (type == A) {
CommonStep1();
AStep1();
CommonStep2();
AStep2();
CommonStep3();
} else if (type == B) {
CommonStep1();
BStep1();
CommonStep2();
BStep2();
CommonStep3();
}
真正的问题不是 if-else 本身。
而是:
固定流程和变化步骤混在了一起。
模板方法模式的核心,就是把它们拆开。
三、模板方法模式到底是什么?
模板方法模式可以这样理解:
在父类中定义一个算法流程骨架,把某些具体步骤延迟到子类中实现。
再说得直白一点:
父类负责规定"先做什么、后做什么",子类负责实现"某一步具体怎么做"。
比如诊断服务处理:
text
父类规定流程:
检查会话 -> 检查安全 -> 校验参数 -> 执行业务 -> 构造响应 -> 记录日志
子类实现差异:
ReadDIDService 实现 ReadDID 的参数校验和业务执行
WriteDIDService 实现 WriteDID 的参数校验和业务执行
ClearDTCService 实现 ClearDTC 的参数校验和业务执行
这就是模板方法模式。
四、模板方法模式解决的核心问题
1. 复用固定流程
流程中稳定的部分,只写一次。
比如:
text
记录开始日志
权限校验
参数校验
执行业务
异常处理
记录结束日志
这些可以放到父类的模板方法中。
2. 让变化步骤由子类扩展
不同服务的差异,放到子类中。
比如:
text
ReadDidService 校验 DID
WriteDidService 校验写入数据
RoutineControlService 校验例程 ID
ClearDtcService 校验故障码分组
新增一种服务时,只需要新增一个子类。
3. 保证流程顺序一致
很多流程是不能让子类随便改顺序的。
比如诊断服务不能先执行再校验权限。
远控指令不能先控制车辆再检查用户权限。
OTA 安装不能先安装再校验包完整性。
模板方法可以把流程顺序固定在父类里。
4. 避免重复代码
如果每个子类都自己写完整流程,很容易出现大量重复。
更严重的是,有些子类可能漏掉某个公共步骤。
比如:
text
某个诊断服务忘了检查安全访问
某个远控命令忘了记录审计日志
某个 OTA 流程忘了上报执行结果
模板方法可以把这些公共步骤统一收敛。
五、模板方法模式的核心角色
模板方法模式通常有两个核心角色。
1. AbstractClass:抽象父类
父类定义模板方法,也就是固定流程。
比如:
cpp
void Handle() {
Step1();
Step2();
Step3();
}
其中某些步骤由父类实现,某些步骤由子类实现。
2. ConcreteClass:具体子类
子类实现变化步骤。
比如:
text
ReadDidService
WriteDidService
ClearDtcService
StartRoutineService
它们不改变整体流程,只补充某些步骤的具体实现。
六、模板方法模式的结构
可以用一个简化结构理解:
text
AbstractClass
├── TemplateMethod()
├── CommonStep()
├── AbstractStep()
└── HookStep()
ConcreteClass
└── 实现 AbstractStep()
关键点是:
模板方法在父类中定义流程,子类不能随意改变流程,只能实现流程中的变化点。
在 C++ 里,模板方法通常会写成非虚函数:
cpp
class BaseProcessor {
public:
void Process() {
Step1();
Step2();
Step3();
}
protected:
virtual void Step1() = 0;
virtual void Step2() = 0;
virtual void Step3() = 0;
};
Process() 就是模板方法。
七、一个 C++ 示例:诊断服务处理流程
先定义请求和响应:
cpp
#include <iostream>
#include <string>
struct DiagnosticRequest {
int sid = 0;
std::string payload;
};
struct DiagnosticResponse {
bool positive = true;
std::string data;
std::string errorCode;
};
定义诊断服务基类:
cpp
class DiagnosticService {
public:
virtual ~DiagnosticService() = default;
DiagnosticResponse Handle(const DiagnosticRequest& request) {
std::cout << "[Diag] start service: "
<< ServiceName()
<< "\n";
DiagnosticResponse response;
if (!CheckSession(request)) {
response.positive = false;
response.errorCode = "NRC: session not supported";
LogResult(response);
return response;
}
if (NeedSecurityAccess() && !CheckSecurity(request)) {
response.positive = false;
response.errorCode = "NRC: security access denied";
LogResult(response);
return response;
}
if (!ValidateRequest(request)) {
response.positive = false;
response.errorCode = "NRC: invalid request";
LogResult(response);
return response;
}
response = ExecuteService(request);
LogResult(response);
return response;
}
protected:
virtual std::string ServiceName() const = 0;
virtual bool ValidateRequest(const DiagnosticRequest& request) = 0;
virtual DiagnosticResponse ExecuteService(const DiagnosticRequest& request) = 0;
virtual bool NeedSecurityAccess() const {
return true;
}
bool CheckSession(const DiagnosticRequest& request) {
std::cout << "[Diag] check session\n";
return true;
}
bool CheckSecurity(const DiagnosticRequest& request) {
std::cout << "[Diag] check security access\n";
return true;
}
void LogResult(const DiagnosticResponse& response) {
if (response.positive) {
std::cout << "[Diag] success, data = "
<< response.data
<< "\n";
} else {
std::cout << "[Diag] failed, error = "
<< response.errorCode
<< "\n";
}
}
};
这里 Handle() 就是模板方法。
它固定了诊断服务处理流程:
text
开始日志
检查会话
检查安全权限
校验请求
执行业务
记录结果
子类不能随便改变这个流程。
接着定义读取 DID 服务:
cpp
class ReadDidService : public DiagnosticService {
protected:
std::string ServiceName() const override {
return "ReadDID";
}
bool ValidateRequest(const DiagnosticRequest& request) override {
std::cout << "[ReadDID] validate DID\n";
return !request.payload.empty();
}
DiagnosticResponse ExecuteService(const DiagnosticRequest& request) override {
std::cout << "[ReadDID] read data by DID\n";
DiagnosticResponse response;
response.positive = true;
response.data = "vehicle_speed=80";
return response;
}
bool NeedSecurityAccess() const override {
return false;
}
};
再定义写 DID 服务:
cpp
class WriteDidService : public DiagnosticService {
protected:
std::string ServiceName() const override {
return "WriteDID";
}
bool ValidateRequest(const DiagnosticRequest& request) override {
std::cout << "[WriteDID] validate DID and data\n";
return request.payload.size() >= 4;
}
DiagnosticResponse ExecuteService(const DiagnosticRequest& request) override {
std::cout << "[WriteDID] write data\n";
DiagnosticResponse response;
response.positive = true;
response.data = "write success";
return response;
}
};
调用方:
cpp
int main() {
DiagnosticRequest readRequest;
readRequest.sid = 0x22;
readRequest.payload = "F190";
DiagnosticRequest writeRequest;
writeRequest.sid = 0x2E;
writeRequest.payload = "F190=VIN123";
ReadDidService readDid;
WriteDidService writeDid;
readDid.Handle(readRequest);
writeDid.Handle(writeRequest);
return 0;
}
输出类似:
text
[Diag] start service: ReadDID
[Diag] check session
[ReadDID] validate DID
[ReadDID] read data by DID
[Diag] success, data = vehicle_speed=80
[Diag] start service: WriteDID
[Diag] check session
[Diag] check security access
[WriteDID] validate DID and data
[WriteDID] write data
[Diag] success, data = write success
这个例子里,父类控制流程,子类实现差异。
这就是模板方法模式的基本用法。
八、模板方法里的钩子方法
模板方法里经常会有一种方法,叫钩子方法。
钩子方法的特点是:
父类提供默认实现,子类可以选择性重写。
比如刚才的:
cpp
virtual bool NeedSecurityAccess() const {
return true;
}
默认需要安全访问。
但 ReadDidService 可以重写:
cpp
bool NeedSecurityAccess() const override {
return false;
}
这就是钩子方法。
它适合处理这种场景:
text
大多数子类都需要某一步
少数子类不需要
或者少数子类需要改变默认行为
常见钩子包括:
text
是否需要权限校验
是否需要安全访问
是否需要记录详细日志
是否允许重试
是否需要回滚
执行前额外检查
执行后额外处理
钩子方法很有用,但不能滥用。
如果一个父类里到处都是钩子,说明流程可能已经不够稳定。
九、再看一个车企例子:远程控制流程
远程控制指令的流程通常也是固定的:
text
解析请求
↓
检查用户权限
↓
检查车辆在线
↓
检查车辆状态
↓
执行控制动作
↓
等待执行结果
↓
上报结果
↓
记录审计日志
不同远控指令变化的是执行动作:
text
UnlockRemoteCommand:解锁
LockRemoteCommand:闭锁
StartAcRemoteCommand:开空调
FindCarRemoteCommand:闪灯鸣笛
CloseWindowRemoteCommand:关窗
可以抽象成模板方法:
cpp
class RemoteControlTask {
public:
virtual ~RemoteControlTask() = default;
void Run() {
LogStart();
if (!CheckPermission()) {
ReportFailed("permission denied");
return;
}
if (!CheckVehicleOnline()) {
ReportFailed("vehicle offline");
return;
}
if (!CheckVehicleState()) {
ReportFailed("vehicle state not allowed");
return;
}
bool success = ExecuteControl();
if (success) {
ReportSuccess();
} else {
ReportFailed("execute failed");
}
LogEnd();
}
protected:
virtual std::string Name() const = 0;
virtual bool ExecuteControl() = 0;
virtual bool CheckVehicleState() {
std::cout << "[" << Name() << "] check vehicle state\n";
return true;
}
private:
void LogStart() {
std::cout << "[" << Name() << "] start remote control\n";
}
bool CheckPermission() {
std::cout << "[" << Name() << "] check permission\n";
return true;
}
bool CheckVehicleOnline() {
std::cout << "[" << Name() << "] check vehicle online\n";
return true;
}
void ReportSuccess() {
std::cout << "[" << Name() << "] report success\n";
}
void ReportFailed(const std::string& reason) {
std::cout << "[" << Name() << "] report failed: "
<< reason
<< "\n";
}
void LogEnd() {
std::cout << "[" << Name() << "] end remote control\n";
}
};
具体远控任务:
cpp
class UnlockRemoteTask : public RemoteControlTask {
protected:
std::string Name() const override {
return "UnlockRemoteTask";
}
bool ExecuteControl() override {
std::cout << "[Door] unlock vehicle\n";
return true;
}
};
cpp
class StartAcRemoteTask : public RemoteControlTask {
protected:
std::string Name() const override {
return "StartAcRemoteTask";
}
bool CheckVehicleState() override {
std::cout << "[StartAc] check battery and power mode\n";
return true;
}
bool ExecuteControl() override {
std::cout << "[HVAC] start air conditioner\n";
return true;
}
};
这个设计的好处是:
text
权限校验不会被某个子类漏掉
车辆在线检查统一处理
结果上报统一处理
审计日志统一处理
不同远控任务只关心自己的控制动作
十、再看一个车企例子:OTA 包处理流程
OTA 包处理通常也有固定流程:
text
下载升级包
↓
校验完整性
↓
校验签名
↓
检查车辆条件
↓
安装升级包
↓
验证安装结果
↓
失败回滚
↓
上报结果
但不同升级对象的安装细节不同:
text
MCU 升级
座舱应用升级
地图数据升级
配置文件升级
ECU 固件升级
可以设计一个 OTA 安装模板:
cpp
class OtaInstallTemplate {
public:
virtual ~OtaInstallTemplate() = default;
bool Install() {
Log("start OTA install");
if (!VerifyPackage()) {
Log("package verify failed");
return false;
}
if (!CheckVehicleCondition()) {
Log("vehicle condition not allowed");
return false;
}
if (!DoInstall()) {
Log("install failed");
if (NeedRollback()) {
Rollback();
}
return false;
}
if (!VerifyInstallResult()) {
Log("install result verify failed");
if (NeedRollback()) {
Rollback();
}
return false;
}
Log("OTA install success");
return true;
}
protected:
virtual bool DoInstall() = 0;
virtual bool VerifyPackage() {
Log("verify package integrity and signature");
return true;
}
virtual bool CheckVehicleCondition() {
Log("check battery, gear, speed and power mode");
return true;
}
virtual bool VerifyInstallResult() {
Log("verify install result");
return true;
}
virtual bool NeedRollback() const {
return true;
}
virtual void Rollback() {
Log("rollback");
}
void Log(const std::string& message) const {
std::cout << "[OTA] " << message << "\n";
}
};
具体安装:
cpp
class AppOtaInstaller : public OtaInstallTemplate {
protected:
bool DoInstall() override {
Log("install cockpit application package");
return true;
}
bool NeedRollback() const override {
return false;
}
};
cpp
class EcuOtaInstaller : public OtaInstallTemplate {
protected:
bool DoInstall() override {
Log("flash ECU firmware");
return true;
}
void Rollback() override {
Log("rollback ECU firmware to previous version");
}
};
这里模板方法固定了 OTA 安装主流程。
不同安装器只关心自己的安装细节。
十一、模板方法和策略模式有什么区别?
模板方法和策略模式都能处理"变化"。
但它们处理变化的方式不一样。
模板方法模式
模板方法是:
父类固定流程,子类改某些步骤。
它依赖继承。
比如:
text
DiagnosticService
├── ReadDidService
├── WriteDidService
└── ClearDtcService
流程在父类中固定。
策略模式
策略模式是:
把可替换算法封装成独立策略,通过组合切换算法。
它依赖组合。
比如:
text
EnergyRecoveryStrategy
├── EcoRecoveryStrategy
├── ComfortRecoveryStrategy
└── SportRecoveryStrategy
调用方可以运行时替换策略。
一句话区分
模板方法:流程固定,步骤变化。
策略模式:算法可替换,流程不一定由父类控制。
如果你想固定执行顺序,用模板方法。
如果你想在运行时替换某个算法,用策略模式。
十二、模板方法和命令模式有什么区别?
命令模式关注:
把一次请求封装成对象,让它可以被排队、记录、撤销、重试。
模板方法关注:
把一个固定流程抽象出来,让子类实现其中的变化步骤。
比如远程控制场景:
text
UnlockCommand 是一个命令对象
RemoteControlTask::Run() 是一个模板方法
两者可以配合使用。
一个命令的 Execute() 内部,可以使用模板方法固定执行流程:
cpp
void Execute() {
RunTemplate();
}
一句话区分:
命令模式:请求对象化。
模板方法:流程骨架复用。
十三、模板方法和责任链模式有什么区别?
责任链模式关注:
请求沿着一条处理链逐级处理。
比如:
text
权限校验 -> 参数校验 -> 安全校验 -> 执行业务
每个节点可以决定是否继续往下传。
模板方法关注:
父类固定整体流程,子类实现某些步骤。
责任链更灵活,步骤可以动态组合。
模板方法更固定,流程顺序更稳定。
一句话区分:
模板方法:流程固定在父类里。
责任链:流程由处理链组合出来。
如果流程长期稳定,用模板方法。
如果流程经常调整、节点可插拔,用责任链。
十四、模板方法最容易被滥用在哪里?
1. 为了复用几行代码就强行继承
如果只是两个类有几行重复代码,不一定需要模板方法。
可以先考虑:
text
提取公共函数
组合工具类
使用策略
使用函数对象
模板方法引入继承关系,会让类之间耦合更强。
2. 父类越来越胖
模板方法最常见的问题是父类膨胀。
一开始父类只是固定流程。
后来不断加:
text
权限校验
状态校验
日志
上报
重试
回滚
异常处理
缓存
指标统计
灰度开关
最后父类变成一个巨大的上帝类。
这时要考虑拆分公共能力。
比如用:
text
装饰器处理日志
责任链处理校验
策略处理变化算法
观察者处理事件通知
不要所有东西都塞进模板父类。
3. 钩子方法太多
如果父类里全是:
cpp
BeforeStep()
AfterStep()
NeedStepA()
NeedStepB()
OnFailed()
OnSuccess()
CanRetry()
ShouldReport()
子类会很难理解。
钩子太多说明流程可能并不稳定。
模板方法适合流程相对固定的场景,不适合流程高度动态的场景。
4. 子类被迫实现不需要的方法
如果父类定义了很多抽象步骤:
cpp
virtual void StepA() = 0;
virtual void StepB() = 0;
virtual void StepC() = 0;
virtual void StepD() = 0;
但很多子类只需要其中一两个,就会出现大量空实现。
这说明抽象层次可能不对。
5. 子类绕开父类流程
模板方法的价值在于父类控制流程。
如果子类可以随便重写主流程方法:
cpp
virtual void Run();
那流程就不再固定了。
所以模板方法通常建议把主流程方法设计为非虚函数。
在 Java 里可以用 final。
在 C++ 里可以通过约定,或者把可变点放到 protected virtual 方法里,主流程保持非虚。
6. 继承层次过深
如果出现:
text
BaseProcessor
↓
VehicleProcessor
↓
RemoteControlProcessor
↓
UnlockProcessor
继承层次过深,理解成本会很高。
模板方法适合一层或少量层次的流程复用。
不要把它发展成复杂继承树。
十五、工程中更推荐的用法
1. 先确认流程真的稳定
模板方法适合这种场景:
text
流程顺序长期稳定
步骤之间有明确先后关系
大部分子类都遵守同一套流程
变化点集中在少数步骤
如果流程经常变化,不要急着用模板方法。
2. 模板方法负责流程,不负责所有细节
父类应该控制主流程:
text
先校验
再执行
再处理结果
最后记录日志
但不要把所有业务细节都写进父类。
父类越稳定,模板方法越有价值。
3. 抽象步骤要少而清楚
变化点不要设计太细。
比如下面这种就太碎:
cpp
BeforeCheck();
DoCheckPart1();
DoCheckPart2();
AfterCheck();
BeforeExecute();
DoExecutePart1();
DoExecutePart2();
AfterExecute();
更好的方式是保留清晰的大步骤:
cpp
ValidateRequest();
ExecuteBusiness();
BuildResponse();
4. 钩子方法要有默认行为
钩子方法最好有合理默认实现。
比如:
cpp
virtual bool NeedSecurityAccess() const {
return true;
}
这样子类只在需要改变行为时重写。
不要让每个子类都被迫实现一堆钩子。
5. 关键流程不要允许子类改顺序
比如远控指令:
text
权限校验必须在执行控制前
车辆状态检查必须在执行控制前
审计日志必须记录
这些就应该放在模板方法里统一控制。
子类只实现真正变化的业务动作。
6. 和其他模式组合使用
模板方法很适合和其他模式组合:
text
模板方法 + 策略模式:固定流程中某一步算法可替换
模板方法 + 命令模式:命令执行内部使用固定流程
模板方法 + 责任链模式:模板里的校验步骤交给责任链
模板方法 + 观察者模式:流程完成后通知订阅者
模板方法 + 工厂模式:根据请求创建具体模板子类
设计模式不是孤立使用的。
真正工程里,经常是组合使用。
十六、一个更完整的组合示例:远控命令 + 模板方法
命令模式可以把远控请求封装成对象。
模板方法可以固定远控执行流程。
比如:
cpp
class RemoteCommand {
public:
virtual ~RemoteCommand() = default;
void Execute() {
LogStart();
if (!CheckPermission()) {
Fail("permission denied");
return;
}
if (!CheckVehicleCondition()) {
Fail("vehicle condition invalid");
return;
}
if (!DoExecute()) {
Fail("execute failed");
return;
}
Success();
}
protected:
virtual std::string Name() const = 0;
virtual bool DoExecute() = 0;
virtual bool CheckVehicleCondition() {
return true;
}
private:
void LogStart() {
std::cout << "[" << Name() << "] start\n";
}
bool CheckPermission() {
std::cout << "[" << Name() << "] check permission\n";
return true;
}
void Success() {
std::cout << "[" << Name() << "] success\n";
}
void Fail(const std::string& reason) {
std::cout << "[" << Name() << "] failed: "
<< reason
<< "\n";
}
};
具体命令:
cpp
class UnlockCommand : public RemoteCommand {
protected:
std::string Name() const override {
return "UnlockCommand";
}
bool DoExecute() override {
std::cout << "[Door] unlock\n";
return true;
}
};
cpp
class StartAcCommand : public RemoteCommand {
protected:
std::string Name() const override {
return "StartAcCommand";
}
bool CheckVehicleCondition() override {
std::cout << "[AC] check battery and power mode\n";
return true;
}
bool DoExecute() override {
std::cout << "[HVAC] start AC\n";
return true;
}
};
这里:
text
RemoteCommand 是命令对象
Execute() 是模板方法
DoExecute() 是子类变化步骤
CheckVehicleCondition() 是钩子方法
这就是模式组合的典型用法。
十七、模板方法的优缺点
优点
第一,复用固定流程。
公共流程只写一次,减少重复代码。
第二,保证执行顺序一致。
关键步骤由父类统一控制,子类不容易漏掉。
第三,变化点更清楚。
子类只实现自己需要变化的步骤。
第四,适合流程型业务。
诊断、远控、OTA、测试执行、数据处理都很适合。
第五,便于统一加日志、校验和异常处理。
父类模板方法可以统一处理公共逻辑。
缺点
第一,依赖继承,耦合较强。
子类依赖父类流程,父类变化会影响所有子类。
第二,父类容易膨胀。
公共逻辑不断增加,父类可能变成大而全的基类。
第三,灵活性不如组合。
运行时替换某个步骤不如策略模式方便。
第四,钩子过多会让流程难懂。
子类到底改了哪些点,需要仔细追踪。
第五,不适合高度动态流程。
如果流程步骤经常变化,责任链或流程编排可能更合适。
十八、使用模板方法前,先问这 6 个问题
1. 这个业务流程是否相对固定?
如果流程顺序经常变化,模板方法可能不合适。
2. 不同实现之间是否只有部分步骤不同?
如果只是某几个步骤变化,模板方法很合适。
3. 是否需要强制所有子类遵守同一流程?
比如权限校验、车辆状态检查、审计日志不能遗漏,就适合放进模板方法。
4. 公共流程是否有明显复用价值?
如果只是少量重复代码,提取公共函数可能就够了。
5. 变化点是否能清楚抽象出来?
比如:
text
ValidateRequest()
ExecuteBusiness()
BuildResponse()
如果变化点说不清,模板方法会很别扭。
6. 继承关系是否会变复杂?
如果会形成很深的继承树,要谨慎使用。
模板方法适合简单清晰的继承结构。
十九、总结
模板方法模式解决的是:
当一个业务流程整体固定,但其中某些步骤会变化时,如何复用流程骨架,并把变化点交给子类实现。
它不是为了炫耀继承。
它真正想解决的是:
- 多个类有相同的处理流程
- 流程顺序必须保持一致
- 某些步骤在不同子类中实现不同
- 公共校验、日志、异常处理不想重复写
- 不希望子类随意改变关键流程
- 新增实现时只关注变化步骤
一句话概括:
模板方法模式的重点不是"把公共代码放父类",而是"父类固定流程,子类实现变化步骤"。
在车企软件里,它很适合这些场景:
- 诊断服务处理流程
- 远程控制执行流程
- OTA 安装流程
- 数据采集和上报流程
- 自动化测试用例执行流程
- 文件解析流程
- 配置下发流程
- 车辆控制任务执行流程
但它也不适合所有场景。
如果流程并不固定,或者变化点很多,模板方法会变得僵硬。
如果只是替换某个算法,策略模式可能更合适。
如果请求需要排队、撤销、重试,命令模式可能更合适。
如果流程节点需要动态组合,责任链可能更合适。
设计模式真正有价值的地方,不是把代码写成某种固定格式,而是:
你能不能识别出"哪些东西是稳定的流程,哪些东西是可变的步骤"。
如果上一篇命令模式提醒我们:
不要把"明明需要排队、记录、撤销、重试的请求",写成一次随手的函数调用。
那么这一篇模板方法模式提醒我们:
不要把"明明流程固定、只是步骤不同的业务",复制粘贴成一堆相似但不一致的代码。
如果这篇对你有帮助,欢迎点赞、转发、关注。
我们下一篇继续拆设计模式。