背景
在 RocksDB 中,有大量 XXXXFactory
这样的类,例如 TableFactory
,它用来实现 SST 文件的插件化,用户要换一种 SST,只需要配置一个相应的 TableFactory
即可,这其实已经很灵活了。
问题
这种简单的 Factory 机制存在两个问题:
要更换 Factory,用户必须修改代码,这虽然有点繁琐,但不算致命
致命的是:如果要更换的是第三方的 Factory,必须在代码中引入对第三方 Factory 的依赖!
- 如果通过其它语言(例如 Java)使用,还需要专门为第三方依赖实现 binding
当年在 TerarkDB
中,我为了实现无缝集成 TerarkZipTable
,避免用户修改代码,使用了一种非常 Hack 的方案:在 DB::Open
中拦截配置,如果发现了相关的环境变量,就启用 TerarkZipTable
。
这样就允许用户不用修改代码,只需要定义环境变量就能使用 TerarkZipTable
。
这种配置方式实现了 TerarkDB 当时的预期目标,但只是一个简陋的补丁!
作为一个完备的、系统化的解决方案,我们(ToplingDB)期望的插件化,仍以 TableFactory
为例,应该让用户可以这样定义 TableFactory
:
1 | std::string table_factory_class = ReadFromSomeWhere(...); |
传统插件化方案
要在更换 Factory 时只需要修改配置而不用修改代码,我们需要将相应的配置项(例如类名)映射到 Factory(的基类)对象(的创建函数),这样就需要一个保存了这种映射关系的全局映射表。
仍以 RocksDB
的 TableFactory
为例,现有代码大致这样:
1 | class TableFactory { |
我们增加一个全局 map
,把类名映射到 NewXXX
函数,但首先就碰到一个问题:这几个函数的 prototype 是不同的,为了统一化,我们把这些 XXXOptions
序列化为 string
:
1 | TableFactory* NewBlockBasedTableFactoryFromString(const std::string&); |
现在可以开始下一步了,定义一个全局 map
,并注册这三个 Factory
:
1 | std::map<std::string, TableFactory*(*)(const std::string&)> table_factory_map; |
大体框架是这样,但是,具体到细节,大致会是这样:
1 | class TableFactory { |
在某 .cc 文件中的全局作用域(下面三个注册可能分散在每个 Table 各自的 .cc 文件中):
1 | REGISTER_TABLE_FACTORY("BlockBasedTable", NewBlockBasedTableFactoryFromString); |
前面用户代码的调用处改成这样就可以了:
1 | TableFactory::NewTableFactory(table_factory_class, table_factory_options); |
这实际上就是很多成熟系统使用的插件化机制。我们把 AutoReg
放入 TableFactory
类中,作为一个内部类,其原因是为了避免污染外层 namespace, REGISTER_TABLE_FACTORY
用来在全局作用域定义一个 AutoReg
对象,该对象在 main
函数执行之前初始化,定义这么一个宏主要是为了方便、统一化,以及可读性,理论上,不使用 REGISTER_TABLE_FACTORY
而完全手写 AutoReg
也是可以的。
接下来的问题是,RocksDB
有大量这样的 XXXFactory
,对于每个 XXXFactory
,我们都写一套这样的代码,工作量很大,很枯燥,还容易出错。于是我们抽象出一个 Factoryable
的模板类:
1 | template<class Product> |
相应的,前面用户代码的调用处改成这样:
1 | TableFactory::NewProduct(table_factory_class, table_factory_options); |
至此,我们只需要对原版 RocksDB
做少量的修改,就解决了我们的两个问题,一切似乎都很美好。但是,RocksDB
中有很多这样的 XXXFactory
,并且,很多即便不是名为 XXXFactory 的 class,也需要这样的 Factory 机制,例如 Comparator
,例如 EventListener
……
旁路插件化方案
对于我们(ToplingDB)来讲,RocksDB 是上游代码,如果上游能及时地接受我们的修改,传统的这种插件化方案其实已经足够好了。如果只是一两个这样的修改,我们可以努力说服上游接受这些修改,但是我们需要对 RocksDB 中大量的 class 都做这样的修改,上游就很难接受了。
所以,我们能否对原版 RocksDB 不做任何修改,就解决这两个问题呢?
其实只要从传统的思维框架“让 class 拥有 Factory 插件化功能” 转变到 ”为 class 添加 Factory 插件化功能“,前面的 Factoryable 代码都不用做任何修改,只需要改一下其中的宏定义 REGISTER_FACTORY_PRODUCT
:
1 |
为了语义上更合逻辑,我们将 Factoryable 重命名为 PluginFactory,再增加一个全局模板函数:
1 | template<class Product> |
相应的用户代码就是:
1 | NewPluginProduct<TableFactory>(table_factory_class, table_factory_options); |
应用
在 ToplingDB 中,我们使用了这种旁落插件化设计模式,当然,相应的实现代码比这里的 demo 代码要复杂很多。
更进一步,同样是在 ToplingDB 中,我们还支持:
- 对象的旁路序列化
- 对象的 REST API 及 Web 可视化展示/修改
这两个功能完整复用了 PluginFactory,只是额外定义了两个模板类,SeDeFunc:
1 | template<class Object> struct SerDeFunc { |
以及 PluginManipFunc:
1 | template<class Object> struct PluginManipFunc { |