命令模式把一次操作请求封装成对象,使请求能够被保存、传递、排队,并在合适的时候执行。本文结合 EvSim 的事件机制,说明事件如何表达动作,以及事件队列和世界对象如何共同完成调度。
在程序中,执行一个动作最直接的方式,就是调用一个函数。例如,让车辆移动,就调用移动函数;让雷达探测,就调用探测函数。这种方式简单清楚,适合立即执行的操作。
随着系统变复杂,我们往往需要进一步管理这些动作:有的动作要等到指定时间执行,有的动作需要按优先级排序,有的动作要周期性重复,还有的动作在执行前可能被取消。此时,程序需要同时处理动作的实现和动作的组织方式。
例如,让某个组件在仿真时间 10 秒时更新一次,可以被封装成一个事件对象。这个对象记录执行时间、优先级、操作目标,并提供统一的执行入口。系统先保存这个对象,等时间到达,再调用它的执行方法。这样,提出请求和执行请求就可以发生在不同的位置、不同的时间。
在 EvSim 项目中,EvEvent 承担了命令对象的角色。它的核心接口可以简化为下面的形式。
class EvEvent
{
public:
enum class EvDisposition
{
Delete,
Reschedule
};
virtual ~EvEvent() = default;
virtual EvDisposition execute() = 0;
};
这里最关键的是 execute()。不同的事件可以执行完全不同的操作,但都通过这个统一入口被调用。调度代码只需要持有 EvEvent 指针,就能管理多种具体事件。
EvEvent 还包含执行时间 time、优先级 priority、执行开关 shouldExecute 等信息。这些字段让一次操作请求具备了可调度、可控制的形式。
命令模式中的几个角色,在项目中可以这样对应。
|----------|-----------------------------------------|--------------------|
| 模式角色 | 项目中的对应对象 | 职责 |
| 命令接口 | EvEvent | 规定统一的执行入口 |
| 具体命令 | EvComponentUpdateEvent EvOneShotEvent 等 | 实现具体操作 |
| 调用者 | EvWorld | 在合适的时机调用 execute() |
| 接收者 | EvComponent 或回调操作的业务对象 | 完成实际业务工作 |
| 请求创建方 | 组件调度代码或其他业务代码 | 创建并提交事件 |
这里还需要一个保存待执行命令的地方,项目使用的是 EvEventQueue。EvEventQueue 内部使用 unique_ptr 持有多个事件对象,并按照执行时间、优先级、入队顺序排序。时间越早,越先出队;时间相同,优先级数值越小,越先出队;前两项都相同,则按照入队先后排序。EvEventQueue 是命令的组织容器,EvWorld 承担执行调度的职责。仿真推进时,EvWorld 从队列中取出到期事件,检查其是否允许执行,再调用 execute()。单线程调度路径逐个执行事件;并行调度路径还会根据优先级和 affinityKey 进行分批与分组。
以组件周期更新为例,组件通过调度代码创建 EvComponentUpdateEvent,把更新请求加入队列。到达计划时间后,世界对象调用事件的 execute(),事件再调用组件的更新逻辑。下面是其中的关键流程,省略了生命周期和状态检查。
// 执行本次组件更新
if (!com->executeScheduledUpdate(time(), mEpoch))
{
return EvDisposition::Delete;
}
// 安排下一次执行时间
setTime(time() + com->updateInterval());
return EvDisposition::Reschedule;
如果组件的更新间隔是 0.1 秒,那么一次执行结束后,事件会把自己的执行时间增加 0.1 秒,并返回 Reschedule。EvWorld 收到这个结果,再把事件放回队列,于是形成连续的周期更新。当组件不再满足执行条件时,事件返回 Delete,该事件对象随之释放。
这个过程体现了命令模式的价值:业务对象专注于具体动作,调度系统统一管理执行时机,以及执行后的处理方式。
组件负责更新自身状态,事件负责包装这次更新请求,队列负责组织待执行请求,世界对象负责推动执行。各部分通过明确的接口协作,减少了调度代码对具体业务的依赖。
以后增加一种新的操作,例如某个时刻触发一次探测,只要提供相应的具体事件,并提交给现有调度机制,就可以复用事件队列和执行流程。调度系统仍然通过统一的 execute() 接口调用它。
命令也可以包装函数,而不必为每一个小动作都编写新的事件类。项目中的 EvOneShotEvent 接收一个回调,执行后结束;EvRecurringEvent 则允许回调决定是否再次调度。这使系统既能使用专门的事件类表达复杂行为,也能用回调表达简单操作。
排队、延迟执行、周期调度,是这个项目在命令对象基础上实现的能力。其他系统还可以为命令增加执行记录、重试或撤销功能,但这些都需要额外设计。例如,要撤销一次操作,就需要保存操作前的状态,或提供能够恢复状态的逆向操作。当前 EvEvent 的公共接口主要围绕执行与重新调度展开。
命令模式也有使用成本。一次简单调用被包装成对象后,会增加事件创建、对象生命周期和调用路径的管理工作。适合采用它的场景,通常是操作请求确实需要统一排队、延后执行或集中调度。
在 EvSim 中,仿真时间推进、事件优先级和组件周期更新都需要这种统一管理。因此,把动作封装为 EvEvent,再交给 EvEventQueue 和 EvWorld 处理,让不同的仿真行为能够接入同一套调度机制。后续扩展新的行为时,主要增加具体事件及其业务逻辑,已有调度流程就能继续发挥作用。