Apollo模板抽象工厂模式解读
[TOC]
之前一篇博文里挖了个坑, 这次来填一下. 本文解读一下 Apollo 里 task 执行的模板抽象工厂模式.
综述
规划的原子操作也可以叫做动作执行元素是task
. 由于 task
太多, Apollo 里进行了 3 步抽象.
第一步: 抽象类: Task base
类.
第二步: Task
类的子类次抽象类, 主要分为三种类: Optimizer
类, Decider
类以及 Learning
类:
1 | SpeedOptimizer, |
第三步: 对三个大类进一步地细分. 例如 Decider
类下面有 STBoundsDecider
, SpeedBoundsDecider
, RuleBasedStopDecider
, PathLaneBorrowDecider
等 12 个子类.
最后每个子类通过参数 const TaskConfig &config,const std::shared_ptr<DependencyInjector> &injector
即 Task 的 config
( protobuf 消息), 以及输入依赖 DependencyInjector
去进一步执行细分的具体的 task
.
那么问题来了这么多类, 如何在运行时动态地产生管理呢? 总不能每次都用 swtich case 语句进行判断然后生成所需要的类.
由于产生类的具体(组装)过程, 我们并不关心, 只想要最终的类的结果, 因此建造者模式不适用, 采用抽象工厂模式. 为了对生成类对象进行管理, 采用了std::unordered_map
存储/查询生成的对象. 同时为了生产不同类型的类, 采取了模板模式, 最终结合为模板抽象工厂模式.
模板抽象工厂模式详解
下图为 Apollo 中整个模式的 UML 图(基于 PlantUML).
下面对其进行一步步拆解.
抽象模板工厂 base 类
1 | //FILE_PATH:modules/common/util/factory.h |
Factory
存在 4 个模板参数:
typename IdentifierType
: 对象 id, 即产品容器std::unordered_map
的 key.class AbstractProduct
: 抽象产品(类), 最终生产出来的类.class ProductCreator
: 函数指针, 输入为void
, 输出为AbstractProduct
对象的函数指针. 这个函数执行具体的生产过程.class MapContainer
: key 为IdentifierType
, value 为ProductCreator
的管理容器, 方便查询某个类是否已经被制造出来.
Factory
有两类操作:
- 对
std::unordered_map
容器的操作: Register(插入), Contains(查询), Unregister(删除), Clear(清空), Empty(判断为空). - 生产模板函数
CreateObject
(调用CreateObjectOrNull
): 逻辑也很简单, 先查询容器里是否已经制造了某类的对象, 如果已经制造好了, 直接去容器里拿, 如果查询不到就地生产一个出来并放入容器里.
生产的函数 CreateObjectOrNull
第一眼看起来很复杂, 其实也很简单.
- 模板参数:
<typename... Args>
不定参数, 因为每个类的生产函数的输入参数都不一样. - 返回值:
std::unique_ptr<AbstractProduct>
抽象产品的唯一智能指针, 使用智能指针不需要手动 new~delete,由系统来管理产品最终的生命周期. 同时使用std::unique_ptr
保证了指针指向单一性, 所以这里不用声明为static
函数. std::forward<Args>(args)...)
: 完美转发保证参数的左右值引用性不会被改变.- return 语句整体为, 先基于输入的参数, 使用容器里的 key (制造函数)制造出一个抽象产品类的对象, 并用一个
std::unique_ptr
智能指针去指向它.
抽象工厂类
首先注意到的是 TaskFactory
不是继承 Factory
的, 而是采用了聚合的形式 ( private 的 Factory
类的实例 task_factory_
, 符合合成复用原则.
所有的函数以及成员变量均为 static
, 保证单例或者只执行一次.
1 | //FILE_PATH:modules/planning/tasks/task_factory.h |
有两点需要注意:
Init
函数: 初始化TaskFactory
类, 做了 2 件事情.一是把当前代码中的
Task
的 Derived 类都 Register 到容器中方便后面直接查询使用, 提高效率. 其中使用 lambda 表达式new
出来一个对象. 总共 Register 了 25 个类.1
2
3
4
5
6task_factory_.Register(
TaskConfig::OPEN_SPACE_TRAJECTORY_PROVIDER,
[](const TaskConfig& config,
const std::shared_ptr<DependencyInjector>& injector) -> Task* {
return new OpenSpaceTrajectoryProvider(config, injector);
});二是把
const PlanningConfig& *config*
里的 task 也拿出来放到容器中, 实现更灵活的添加方式.综合两点来看起来制造函数已经不需要了, 因为所有需要制造的实例都已经
new
出来放到容器里了. 但是对于更一般的场景还是需要下面的函数来应对.CreateTask
函数为制造实例的方法. 先把TaskConfig
与PlanningConfig
里的 task 都放到容器里, 然后调用 base 类的CreateObject
函数创建对象.其中
MergeFrom
为 protobuf 的合并函数 API.1
2
3
4
5
6
7
8
9
10
11
12std::unique_ptr<Task> TaskFactory::CreateTask(
const TaskConfig& task_config,
const std::shared_ptr<DependencyInjector>& injector) {
TaskConfig merged_config;
if (default_task_configs_.find(task_config.task_type()) !=
default_task_configs_.end()) {
merged_config = default_task_configs_[task_config.task_type()];
}
merged_config.MergeFrom(task_config);
return task_factory_.CreateObject(task_config.task_type(), merged_config,
injector);
}
抽象产品类
Task
类, 没有 private 成员, 所有成员均可被 public 继承. 可以讲的有 2 处.
- 成员变量: 均为 Planning 模块的核心输入类指针, 例如
Frame,ReferenceLineInfo,TaskConfig,DependencyInjector
. Execute
成员函数负责执行后续每个 task 的具体任务内容, 为了实现多态, 两个重载的函数均为virtual
.
1 | class Task { |
次抽象产品类
以 TrajectoryOptimizer
为例, override 了父类的 Execute
函数(这里没有再往下传递这个函数里, 取而代之的是 Process
, 子类里的执行函数入参不够满足子类的复杂度要求). 没有 private 成员, 所有成员均可被 public 继承. 同时增加了新的虚函数 Process
强制子类必须重载之.
1 | class TrajectoryOptimizer : public Task { |
产品类
以 OpenSpaceTrajectoryProvider
为例, 重载了 Process
函数, 实现多态.
1 | class OpenSpaceTrajectoryProvider : public TrajectoryOptimizer { |
总结
Apollo 的这套模板抽象工厂模式有如下优点(我认为设计模式是可变可组合的, 只有设计原则是不变的):
- 符合开闭原则, 后面即便有新的 task 要加入进来, 也只需要使用跟大多数现存 task 类相同的接口
const TaskConfig& config, const std::shared_ptr<DependencyInjector>& injector
在TaskFactory
中 Register 即可. 如果无法使用一样的接口也可以另起炉灶使用模板基类的CreateObject
函数自定义制造函数. - 如前文所说部分符合合成复用原则,
TaskFactory
通过复合的方式联合Factory
. - 符合单一职责原则, 每个类的定位单一, 整体层次明晰.
- 由于产品类内部的成员变量基本为相同的类指针(或者是智能指针)也间接地实现了享元模式.
也有些我觉得可以提升的地方:
- register 的部分可以考虑使用接口实现, 可以参考知乎文章, 这样可以让产品继承/聚合接口类. 这样就不会出现几百行的 register 重复类似代码.
- 可以考虑所有的产品类都继承自一个抽象产品类而不是先抽象一波再次抽象一波. 至于不同类的
Process
不同可以设置专门的Process
接口类, 通过继承/重载实现不同功能. 当然 Apollo 这么分也有可能是为了语义/分工上的现实意义.
下图为改进后的 UML 示意图, 其中 task 的各产品类聚合了 Interface 产品制造方法, 并且各自的构造函数被禁止(private 属性).
至于更上一层的 scenario stage 等的调用机制以及 Task 的具体执行过程, 这里先挖个坑后面填.
Apollo模板抽象工厂模式解读