命令模式:把请求封装成对象,有什么实际价值

上一篇讲了观察者模式。

它解决的问题是:

当一个对象发生变化后,需要通知多个模块,但又不希望发布者直接依赖所有订阅者时,如何解耦通知关系。

比如车辆信号变化、OTA 进度变化、告警事件、诊断结果、配置变化,都可以通过观察者模式通知多个模块。

观察者模式的重点是:

发布者只发布事件,不依赖具体接收方。

这一篇讲命令模式。

它解决的问题是:

当一个请求不仅仅是"立刻调用一个函数",而是需要被排队、记录、撤销、重试、异步执行、权限校验或统一调度时,应该怎么设计。

车企软件里,这种场景非常多:

  • HMI 按钮点击后触发车控动作
  • 远程控制指令排队执行
  • OTA 下载、校验、安装任务调度
  • 诊断请求统一封装和执行
  • 自动化测试用例步骤执行
  • 座舱里用户操作的撤销和重做
  • 车辆控制指令的日志记录和审计
  • 多个模块通过任务队列异步执行动作

这些场景有一个共同点:

请求本身需要被当成一个对象来管理。

这就是命令模式的价值。


一、先从一个车企场景说起

假设我们现在做一个远程车控模块。

用户在 App 上可以发很多指令:

text 复制代码
远程解锁
远程闭锁
远程开空调
远程关空调
远程寻车
远程开窗
远程关窗

如果直接写,代码可能是这样:

cpp 复制代码
void RemoteControlService::HandleRequest(const RemoteRequest& request) {
    if (request.type == "Unlock") {
        doorController_->Unlock();
    } else if (request.type == "Lock") {
        doorController_->Lock();
    } else if (request.type == "StartAc") {
        hvacController_->Start();
    } else if (request.type == "StopAc") {
        hvacController_->Stop();
    } else if (request.type == "FindCar") {
        lightController_->Flash();
        hornController_->Beep();
    }
}

一开始看起来没问题。

但远控业务很快会变复杂:

text 复制代码
每条指令要做权限校验
每条指令要记录日志
每条指令可能需要排队
每条指令可能需要重试
每条指令有超时时间
每条指令执行前要检查车辆状态
每条指令执行后要上报云端
有些指令可以取消
有些指令需要异步等待结果
有些指令失败后要回滚

如果继续用 if-else 写,代码会变成:

cpp 复制代码
if (request.type == "Unlock") {
    CheckPermission();
    CheckVehicleState();
    LogStart();
    doorController_->Unlock();
    WaitResult();
    ReportResult();
} else if (request.type == "StartAc") {
    CheckPermission();
    CheckVehicleState();
    LogStart();
    hvacController_->Start();
    WaitResult();
    ReportResult();
}

大量重复流程和业务细节会混在一起。

命令模式要做的事情就是:

把"远程解锁""远程开空调""远程寻车"这些请求封装成一个个命令对象,让调度器只负责执行命令,而不是关心每个命令具体怎么做。


二、为什么要把请求封装成对象?

很多人第一次看到命令模式会觉得奇怪:

我直接调用函数不就行了吗?

为什么还要包一层 Command?

简单场景下,直接调用当然更好。

比如:

cpp 复制代码
doorController.Unlock();

这很清楚。

但如果请求有自己的生命周期,就不再只是一次函数调用了。

比如一个远控指令可能要经历:

text 复制代码
创建
校验
排队
执行
等待结果
重试
取消
记录日志
上报结果

这时候,请求本身就变成了一个需要管理的对象。

命令模式的核心就是:

把一次请求封装成一个对象,让它可以像普通对象一样被传递、保存、排队、撤销、重试和记录。


三、命令模式到底是什么?

命令模式可以这样理解:

把请求封装成对象,从而让请求的发送者和执行者解耦。

再说得直白一点:

调用方不直接调用具体业务对象,而是创建一个命令对象,由命令对象去执行真正的动作。

比如远程解锁:

直接调用是:

cpp 复制代码
doorController.Unlock();

命令模式是:

cpp 复制代码
auto command = std::make_unique<UnlockCommand>(doorController);
command->Execute();

看起来只是多包了一层。

但这一层带来的价值是:

text 复制代码
命令可以排队
命令可以记录
命令可以撤销
命令可以重试
命令可以异步执行
命令可以统一调度
命令可以做权限校验
命令可以作为测试步骤

这就是命令模式的实际价值。


四、命令模式解决的核心问题

1. 解耦请求发送者和请求执行者

没有命令模式时,调用方直接依赖具体执行对象:

text 复制代码
RemoteControlService -> DoorController
RemoteControlService -> HvacController
RemoteControlService -> LightController
RemoteControlService -> HornController

用了命令模式后,调用方只依赖命令接口:

text 复制代码
RemoteControlService -> ICommand

具体命令内部再去调用真正的接收者。

这样调用方不需要知道每种请求背后调用了哪些模块。


2. 让请求可以被排队

函数调用是瞬时行为。

调用完就结束了。

但命令对象可以被放进队列:

cpp 复制代码
commandQueue.Push(std::move(command));

然后由调度线程慢慢执行。

这在车企软件里非常常见。

比如远控指令、诊断任务、OTA 任务、自动化测试步骤,都可能需要排队执行。


3. 让请求可以被记录

命令对象可以记录:

text 复制代码
命令类型
命令参数
创建时间
执行时间
执行结果
失败原因
重试次数

这对问题排查非常重要。

比如线上发现用户远程开空调失败,你至少要知道:

text 复制代码
指令有没有收到
什么时候收到
有没有进入队列
执行到哪一步
失败在哪个模块
有没有重试
最终结果是什么

如果只是普通函数调用,这些信息很容易散落在各处。


4. 让请求可以撤销和重做

有些场景需要撤销操作。

比如座舱编辑配置:

text 复制代码
调整座椅位置
调整空调温度
修改快捷菜单
编辑场景模式

每次操作都封装成命令,就可以实现:

text 复制代码
Undo
Redo

命令模式是撤销 / 重做功能最常见的设计基础。


5. 让请求可以异步执行和重试

命令对象可以被提交给线程池:

cpp 复制代码
executor.Submit(std::move(command));

也可以执行失败后重新入队:

cpp 复制代码
if (command->CanRetry()) {
    queue.Push(std::move(command));
}

这比在业务函数里到处写重试逻辑更容易统一管理。


五、命令模式的核心角色

命令模式通常有几个角色。

1. Command:命令接口

定义所有命令都要实现的统一方法。

通常是:

cpp 复制代码
class ICommand {
public:
    virtual ~ICommand() = default;
    virtual void Execute() = 0;
};

如果支持撤销,可以加:

cpp 复制代码
virtual void Undo() = 0;

2. ConcreteCommand:具体命令

具体命令封装一次具体请求。

比如:

text 复制代码
UnlockCommand
LockCommand
StartAcCommand
StopAcCommand
FindCarCommand
ReadDidCommand
OtaInstallCommand

每个命令知道自己要调用哪个接收者,以及需要哪些参数。


3. Receiver:接收者

真正执行业务动作的对象。

比如:

text 复制代码
DoorController
HvacController
LightController
HornController
DiagnosticClient
OtaManager

命令本身不一定实现底层业务,它通常只是把请求转交给真正的接收者。


4. Invoker:调用者 / 调度者

调用者不关心具体命令怎么执行,只负责触发命令。

比如:

text 复制代码
RemoteControlService
CommandQueue
TaskScheduler
ButtonHandler
TestRunner

它只调用:

cpp 复制代码
command->Execute();

5. Client:命令创建方

负责把接收者和参数组装成命令对象。

比如:

cpp 复制代码
auto command = std::make_unique<UnlockCommand>(doorController);

六、命令模式的结构

可以用一个简化结构理解:

text 复制代码
Client 创建 Command
Command 持有 Receiver
Invoker 执行 Command
Command 调用 Receiver

结构类似:

text 复制代码
Client
  ↓ 创建
Command
  ↓ 调用
Receiver

Invoker
  ↓ 执行
Command

用远控场景表示就是:

text 复制代码
RemoteRequestParser
  ↓ 创建
UnlockCommand / StartAcCommand / FindCarCommand
  ↓ 调用
DoorController / HvacController / LightController

CommandScheduler
  ↓ 执行
ICommand

最关键的是:

调度器只知道自己在执行命令,不知道命令背后到底是解锁、开空调还是寻车。


七、一个 C++ 示例:远程车控命令

先定义命令接口:

cpp 复制代码
#include <iostream>
#include <memory>
#include <queue>
#include <string>

class ICommand {
public:
    virtual ~ICommand() = default;

    virtual void Execute() = 0;
    virtual std::string Name() const = 0;
};

再定义几个真正的业务接收者。

车门控制器:

cpp 复制代码
class DoorController {
public:
    void Unlock() {
        std::cout << "[Door] unlock vehicle\n";
    }

    void Lock() {
        std::cout << "[Door] lock vehicle\n";
    }
};

空调控制器:

cpp 复制代码
class HvacController {
public:
    void Start() {
        std::cout << "[HVAC] start air conditioner\n";
    }

    void Stop() {
        std::cout << "[HVAC] stop air conditioner\n";
    }
};

灯光和喇叭控制器:

cpp 复制代码
class LightController {
public:
    void Flash() {
        std::cout << "[Light] flash lights\n";
    }
};

class HornController {
public:
    void Beep() {
        std::cout << "[Horn] beep horn\n";
    }
};

然后定义具体命令。

远程解锁命令:

cpp 复制代码
class UnlockCommand : public ICommand {
public:
    explicit UnlockCommand(DoorController& door)
        : door_(door) {}

    void Execute() override {
        door_.Unlock();
    }

    std::string Name() const override {
        return "UnlockCommand";
    }

private:
    DoorController& door_;
};

远程闭锁命令:

cpp 复制代码
class LockCommand : public ICommand {
public:
    explicit LockCommand(DoorController& door)
        : door_(door) {}

    void Execute() override {
        door_.Lock();
    }

    std::string Name() const override {
        return "LockCommand";
    }

private:
    DoorController& door_;
};

远程开空调命令:

cpp 复制代码
class StartAcCommand : public ICommand {
public:
    explicit StartAcCommand(HvacController& hvac)
        : hvac_(hvac) {}

    void Execute() override {
        hvac_.Start();
    }

    std::string Name() const override {
        return "StartAcCommand";
    }

private:
    HvacController& hvac_;
};

远程寻车命令:

cpp 复制代码
class FindCarCommand : public ICommand {
public:
    FindCarCommand(LightController& light, HornController& horn)
        : light_(light), horn_(horn) {}

    void Execute() override {
        light_.Flash();
        horn_.Beep();
    }

    std::string Name() const override {
        return "FindCarCommand";
    }

private:
    LightController& light_;
    HornController& horn_;
};

接着定义命令调度器:

cpp 复制代码
class CommandScheduler {
public:
    void Submit(std::unique_ptr<ICommand> command) {
        if (command) {
            queue_.push(std::move(command));
        }
    }

    void Run() {
        while (!queue_.empty()) {
            auto command = std::move(queue_.front());
            queue_.pop();

            std::cout << "[Scheduler] execute "
                      << command->Name()
                      << "\n";

            command->Execute();
        }
    }

private:
    std::queue<std::unique_ptr<ICommand>> queue_;
};

调用方这样使用:

cpp 复制代码
int main() {
    DoorController door;
    HvacController hvac;
    LightController light;
    HornController horn;

    CommandScheduler scheduler;

    scheduler.Submit(std::make_unique<UnlockCommand>(door));
    scheduler.Submit(std::make_unique<StartAcCommand>(hvac));
    scheduler.Submit(std::make_unique<FindCarCommand>(light, horn));
    scheduler.Submit(std::make_unique<LockCommand>(door));

    scheduler.Run();

    return 0;
}

输出类似:

text 复制代码
[Scheduler] execute UnlockCommand
[Door] unlock vehicle

[Scheduler] execute StartAcCommand
[HVAC] start air conditioner

[Scheduler] execute FindCarCommand
[Light] flash lights
[Horn] beep horn

[Scheduler] execute LockCommand
[Door] lock vehicle

这个例子里,CommandScheduler 不知道每条命令具体做什么。

它只负责:

text 复制代码
接收命令
保存命令
执行命令

而每个命令负责自己的业务动作。

这就是命令模式的基本结构。


八、命令模式真正的价值不是多包一层

如果只是为了调用:

cpp 复制代码
command->Execute();

那确实没有必要。

命令模式真正有价值的地方,是命令对象可以被统一管理。

比如我们可以给调度器加日志:

cpp 复制代码
std::cout << "start command: " << command->Name() << "\n";
command->Execute();
std::cout << "finish command: " << command->Name() << "\n";

也可以加异常隔离:

cpp 复制代码
try {
    command->Execute();
} catch (...) {
    std::cout << "command failed: " << command->Name() << "\n";
}

还可以加执行结果:

text 复制代码
Pending
Running
Success
Failed
Canceled
Timeout

这时命令就不再只是一个函数包装器,而是一个可以被调度系统管理的任务单元。


九、给命令加上执行结果

真实工程里,命令执行通常不只是 void Execute()

它可能需要返回结果。

可以这样设计:

cpp 复制代码
enum class CommandResult {
    Success,
    Failed,
    Timeout,
    Rejected
};

命令接口改成:

cpp 复制代码
class ICommand {
public:
    virtual ~ICommand() = default;

    virtual CommandResult Execute() = 0;
    virtual std::string Name() const = 0;
};

调度器就可以统一处理结果:

cpp 复制代码
class CommandScheduler {
public:
    void Submit(std::unique_ptr<ICommand> command) {
        if (command) {
            queue_.push(std::move(command));
        }
    }

    void Run() {
        while (!queue_.empty()) {
            auto command = std::move(queue_.front());
            queue_.pop();

            std::cout << "[Scheduler] start "
                      << command->Name()
                      << "\n";

            CommandResult result = command->Execute();

            if (result == CommandResult::Success) {
                std::cout << "[Scheduler] success\n";
            } else {
                std::cout << "[Scheduler] failed\n";
            }
        }
    }

private:
    std::queue<std::unique_ptr<ICommand>> queue_;
};

这样每个命令可以有自己的执行逻辑,但执行结果由调度器统一处理。


十、命令模式和撤销 / 重做

命令模式还有一个经典用途:撤销和重做。

比如座舱里用户调整空调温度。

每次调整都可以封装成一个命令:

text 复制代码
SetTemperatureCommand

它不仅知道怎么执行,还知道怎么撤销。

示例:

cpp 复制代码
class ClimateController {
public:
    void SetTemperature(double temperature) {
        temperature_ = temperature;
        std::cout << "[Climate] temperature = "
                  << temperature_
                  << "\n";
    }

    double GetTemperature() const {
        return temperature_;
    }

private:
    double temperature_ = 24.0;
};

命令接口支持撤销:

cpp 复制代码
class IUndoableCommand {
public:
    virtual ~IUndoableCommand() = default;

    virtual void Execute() = 0;
    virtual void Undo() = 0;
};

温度设置命令:

cpp 复制代码
class SetTemperatureCommand : public IUndoableCommand {
public:
    SetTemperatureCommand(ClimateController& climate,
                          double targetTemperature)
        : climate_(climate),
          targetTemperature_(targetTemperature),
          oldTemperature_(climate.GetTemperature()) {}

    void Execute() override {
        oldTemperature_ = climate_.GetTemperature();
        climate_.SetTemperature(targetTemperature_);
    }

    void Undo() override {
        climate_.SetTemperature(oldTemperature_);
    }

private:
    ClimateController& climate_;
    double targetTemperature_;
    double oldTemperature_;
};

命令历史:

cpp 复制代码
#include <vector>

class CommandHistory {
public:
    void Execute(std::unique_ptr<IUndoableCommand> command) {
        if (!command) {
            return;
        }

        command->Execute();
        history_.push_back(std::move(command));
    }

    void UndoLast() {
        if (history_.empty()) {
            return;
        }

        history_.back()->Undo();
        history_.pop_back();
    }

private:
    std::vector<std::unique_ptr<IUndoableCommand>> history_;
};

调用:

cpp 复制代码
int main() {
    ClimateController climate;
    CommandHistory history;

    history.Execute(std::make_unique<SetTemperatureCommand>(climate, 22.0));
    history.Execute(std::make_unique<SetTemperatureCommand>(climate, 20.0));

    history.UndoLast();
    history.UndoLast();

    return 0;
}

输出类似:

text 复制代码
[Climate] temperature = 22
[Climate] temperature = 20
[Climate] temperature = 22
[Climate] temperature = 24

这里撤销能力不是靠 ClimateController 自己实现的。

而是每个命令保存了执行前的状态。

这就是命令模式非常实用的地方。


十一、再看一个车企例子:诊断命令

诊断系统里,不同 UDS 服务可以封装成不同命令。

比如:

text 复制代码
ReadDidCommand
WriteDidCommand
ClearDtcCommand
SecurityAccessCommand
RoutineControlCommand

如果诊断执行器直接写:

cpp 复制代码
if (serviceId == 0x22) {
    ReadDid();
} else if (serviceId == 0x2E) {
    WriteDid();
} else if (serviceId == 0x14) {
    ClearDtc();
}

后面会越来越复杂。

用命令模式可以变成:

cpp 复制代码
class IDiagnosticCommand {
public:
    virtual ~IDiagnosticCommand() = default;

    virtual bool Execute() = 0;
    virtual std::string Name() const = 0;
};

读取 DID 命令:

cpp 复制代码
class ReadDidCommand : public IDiagnosticCommand {
public:
    ReadDidCommand(DiagnosticClient& client, uint16_t did)
        : client_(client), did_(did) {}

    bool Execute() override {
        return client_.ReadDid(did_);
    }

    std::string Name() const override {
        return "ReadDidCommand";
    }

private:
    DiagnosticClient& client_;
    uint16_t did_;
};

清除 DTC 命令:

cpp 复制代码
class ClearDtcCommand : public IDiagnosticCommand {
public:
    ClearDtcCommand(DiagnosticClient& client, uint32_t group)
        : client_(client), group_(group) {}

    bool Execute() override {
        return client_.ClearDtc(group_);
    }

    std::string Name() const override {
        return "ClearDtcCommand";
    }

private:
    DiagnosticClient& client_;
    uint32_t group_;
};

诊断执行器只负责:

cpp 复制代码
executor.Submit(std::move(command));

这样诊断命令就可以被:

text 复制代码
排队
限流
记录日志
统一超时
失败重试
生成测试报告

这比每种诊断服务都在执行器里写分支更可维护。


十二、再看一个车企例子:自动化测试步骤

自动化测试里,命令模式也很好用。

一个测试用例通常由多个步骤组成:

text 复制代码
打开车门
启动车辆
设置空调温度
发送诊断请求
检查信号值
关闭车辆

每个步骤都可以是一个命令:

text 复制代码
OpenDoorCommand
StartVehicleCommand
SetHvacCommand
SendDiagnosticCommand
CheckSignalCommand
StopVehicleCommand

测试执行器只需要:

cpp 复制代码
for (auto& step : testCase.steps) {
    step->Execute();
}

这样做的好处是:

text 复制代码
测试步骤可以组合
测试步骤可以复用
执行过程可以记录
失败步骤可以定位
步骤可以从配置文件生成
步骤可以支持回滚或清理动作

命令模式在测试框架里非常常见。


十三、命令模式和策略模式有什么区别?

这两个模式也容易混。

因为它们都可能长这样:

cpp 复制代码
interface {
    Execute();
}

但意图不一样。


策略模式

策略模式关注:

同一个问题有多种算法,运行时选择一种算法。

比如:

text 复制代码
经济模式回收策略
舒适模式回收策略
运动模式回收策略

重点是算法可替换。


命令模式

命令模式关注:

把一次请求封装成对象,让请求可以被传递、排队、记录、撤销、重试。

比如:

text 复制代码
远程解锁命令
远程开空调命令
读取 DID 命令
OTA 安装命令

重点是请求对象化。


一句话区分

策略模式:换一种算法。

命令模式:封装一次请求。

如果你关心的是"怎么算",更像策略模式。

如果你关心的是"这个请求怎么被调度和管理",更像命令模式。


十四、命令模式和责任链模式有什么区别?

责任链模式关注请求如何被多个处理器逐级处理。

比如:

text 复制代码
权限校验 -> 参数校验 -> 安全校验 -> 执行业务

命令模式关注请求本身被封装成对象。

比如:

text 复制代码
UnlockCommand
StartAcCommand
ReadDidCommand

两者可以配合使用。

比如远控命令执行前,可以先经过责任链:

text 复制代码
权限校验处理器
车辆状态校验处理器
频率限制处理器
安全校验处理器

校验通过后,再执行命令:

cpp 复制代码
command->Execute();

一句话区分:

命令模式:请求被封装成对象。

责任链模式:请求沿着处理链流转。


十五、命令模式和观察者模式有什么区别?

观察者模式关注:

一个事件发生后,通知多个订阅者。

比如:

text 复制代码
车速变化 -> 通知 HMI、日志、告警、云端

命令模式关注:

一个请求被封装成对象,由调度者执行。

比如:

text 复制代码
远程解锁命令 -> 调度器执行 -> 门锁控制器解锁

观察者更像"事件广播"。

命令更像"动作请求"。

一句话区分:

观察者模式:发生了什么,通知大家。

命令模式:我要做什么,封装成命令。


十六、命令模式最容易被滥用在哪里?

1. 简单调用也强行命令化

如果只是:

cpp 复制代码
door.Unlock();

而且没有排队、撤销、日志、异步、重试需求,就没必要封装成命令。

命令模式适合请求需要被管理的场景。

不是所有函数调用都要变成命令对象。


2. 命令类太多太碎

如果每个一行函数都封装成一个命令,系统会出现大量很薄的类。

比如:

text 复制代码
TurnOnLightCommand
TurnOffLightCommand
TurnLeftLightCommand
TurnRightLightCommand

如果这些命令没有独立管理价值,就可能过度设计。

可以考虑用参数化命令:

cpp 复制代码
SetLightCommand(LightType type, LightAction action);

3. 命令里塞太多业务流程

有些命令会变成一个大业务类:

text 复制代码
校验权限
查配置
调用控制器
写数据库
通知 HMI
上报云端
发日志
处理失败
重试
回滚

这时要警惕。

命令可以封装请求,但不应该变成"万能业务容器"。

公共流程可以放到调度器、责任链、模板方法或服务层中。


4. 命令和接收者边界不清

命令应该表达:

我要执行一个请求。

接收者应该负责:

真正完成业务动作。

如果命令里自己实现了所有底层控制逻辑,接收者就失去了意义。

比如 UnlockCommand 不应该直接操作硬件细节。

它更应该调用:

cpp 复制代码
doorController.Unlock();

5. 忽略命令生命周期

命令对象可能会被保存、排队、异步执行。

所以要想清楚:

text 复制代码
命令持有的引用是否还有效
命令参数是否需要拷贝
命令能否重复执行
命令是否线程安全
命令失败后是否能重试
命令取消后如何处理

这些都是工程里真正会出问题的地方。


6. 命令执行顺序被误用

如果多个命令之间有依赖关系,比如:

text 复制代码
先解锁
再开窗
再启动空调

不要只依赖队列顺序隐式保证。

最好把它设计成组合命令或任务流程。

否则以后有人调整队列策略,业务可能出问题。


十七、工程中更推荐的用法

1. 先判断请求是否需要"对象化"

适合命令模式的请求通常有这些特征:

text 复制代码
需要排队
需要异步执行
需要记录日志
需要撤销或重做
需要失败重试
需要统一权限校验
需要统一调度
需要跨模块传递
需要从配置生成

如果都不需要,直接调用就好。


2. 命令接口要小

最简单的命令接口通常只需要:

cpp 复制代码
virtual CommandResult Execute() = 0;

不要一开始就设计成:

cpp 复制代码
Init()
PreCheck()
Execute()
Pause()
Resume()
Cancel()
Rollback()
Report()

除非这些确实是所有命令都需要的生命周期。

接口越大,具体命令越难实现。


3. 公共能力放到调度器或装饰器

比如:

text 复制代码
日志
耗时统计
异常捕获
权限校验
重试
超时
上报

不一定要每个命令都重复写。

可以放到命令调度器里统一处理。

也可以用装饰器包装命令:

text 复制代码
LoggingCommandDecorator
RetryCommandDecorator
TimeoutCommandDecorator

这样命令本身只专注业务动作。


4. 命令参数尽量自包含

命令被放进队列后,可能过一段时间才执行。

所以命令需要的参数最好在创建时就确定下来。

不要执行时再依赖一堆外部临时变量。

比如:

cpp 复制代码
ReadDidCommand(client, did);

比下面这种更稳定:

cpp 复制代码
ReadDidCommand(client, requestRef);

如果 requestRef 生命周期不清楚,就容易出问题。


5. 明确命令是否可重复执行

有些命令可以重复执行:

text 复制代码
读取 DID
查询状态
刷新页面

有些命令不能随便重复执行:

text 复制代码
支付
写入配置
远程解锁
刷写 ECU

对于不可重复命令,要考虑幂等性和去重。

尤其是远控和 OTA 场景,一条命令重复执行可能带来实际风险。


6. 关键命令要有审计日志

对于车控、诊断、OTA 这类关键命令,建议记录:

text 复制代码
命令 ID
命令类型
来源
参数摘要
创建时间
开始执行时间
结束时间
执行结果
失败原因
重试次数
车辆状态

否则线上问题很难排查。


十八、组合命令:把多个命令组合成一个流程

有些业务不是一个动作,而是一组动作。

比如远程寻车可能包括:

text 复制代码
闪灯
鸣笛
持续 5 秒
停止闪灯
停止鸣笛

可以设计组合命令:

cpp 复制代码
class CompositeCommand : public ICommand {
public:
    void Add(std::unique_ptr<ICommand> command) {
        if (command) {
            commands_.push_back(std::move(command));
        }
    }

    void Execute() override {
        for (auto& command : commands_) {
            command->Execute();
        }
    }

    std::string Name() const override {
        return "CompositeCommand";
    }

private:
    std::vector<std::unique_ptr<ICommand>> commands_;
};

这样一个复杂动作可以由多个简单命令组合出来。

这在自动化测试、OTA 流程、座舱场景联动里都很有用。


十九、命令模式的优缺点

优点

第一,解耦请求发送者和执行者。

调用方只依赖命令接口,不依赖具体业务模块。

第二,请求可以被统一管理。

命令可以排队、异步执行、记录日志、失败重试。

第三,支持撤销和重做。

命令可以保存执行前状态,从而实现 Undo / Redo。

第四,适合任务调度系统。

远控、诊断、OTA、测试步骤都可以统一抽象为命令。

第五,扩展新命令更容易。

新增请求类型时,可以新增一个命令类,而不是修改一个巨大 if-else。


缺点

第一,类数量会增加。

每种请求都可能对应一个命令类。

第二,简单场景会显得过度设计。

如果只是普通函数调用,命令模式反而增加复杂度。

第三,命令生命周期需要管理。

尤其是异步执行时,要注意引用、资源、线程安全和取消逻辑。

第四,命令和业务边界容易混乱。

命令不应该变成承载所有业务流程的巨型类。

第五,调试链路可能变长。

从调用方到命令,再到调度器,再到接收者,链路比直接调用更长。


二十、使用命令模式前,先问这 6 个问题

1. 这个请求是否需要被排队或异步执行?

如果需要,命令模式很适合。


2. 这个请求是否需要记录、审计或追踪?

如果是远控、诊断、OTA 这类关键动作,命令对象会更容易记录完整生命周期。


3. 这个请求是否需要撤销或重做?

如果需要 Undo / Redo,命令模式是非常自然的选择。


4. 请求发送者是否不应该依赖具体执行者?

如果调用方不应该知道背后有哪些控制器,就可以通过命令解耦。


5. 请求种类未来是否会继续增加?

如果命令类型会持续扩展,命令模式比巨大 if-else 更容易维护。


6. 命令是否有清晰边界?

如果命令边界不清,很容易变成大杂烩。

使用前要想清楚:

text 复制代码
命令负责什么
接收者负责什么
调度器负责什么
公共流程放哪里

二十一、总结

命令模式解决的是:

当一次请求需要被传递、排队、记录、撤销、重试、异步执行或统一调度时,如何把请求封装成对象。

它不是为了把简单函数调用复杂化。

它真正想解决的是:

  • 请求发送者和执行者强耦合
  • 请求需要排队和异步执行
  • 请求需要日志、审计和追踪
  • 请求需要撤销和重做
  • 请求需要失败重试和统一调度
  • 请求类型越来越多,if-else 越来越长
  • 请求需要作为测试步骤或任务单元被组合

一句话概括:

命令模式的重点不是"把函数包成类",而是"让请求变成可以被管理的对象"。

在车企软件里,它很适合这些场景:

  • 远程车控指令
  • HMI 用户操作
  • OTA 下载 / 校验 / 安装任务
  • 诊断请求执行
  • 自动化测试步骤
  • 车辆控制任务队列
  • 座舱撤销 / 重做操作
  • 跨模块异步任务调度
  • 批量任务编排

但它也不适合所有场景。

如果只是一次简单、直接、同步的调用,用普通函数更清楚。

设计模式真正有价值的地方,不是把代码写得更"模式化",而是:

你能不能识别出"这个请求已经不只是一次调用,而是一个需要被管理的任务"。

如果上一篇观察者模式提醒我们:

不要让"明明只是发布一个事件"的模块,直接依赖一堆具体响应模块。

那么这一篇命令模式提醒我们:

不要把"明明需要排队、记录、撤销、重试的请求",写成一次随手的函数调用。

如果这篇对你有帮助,欢迎点赞、转发、关注。

我们下一篇继续拆设计模式。