背景
在 RocksDB 中,有大量 XXXXFactory
这样的类,例如 TableFactory
,它用来实现 SST 文件的插件化,用户要换一种 SST,只需要配置一个相应的 TableFactory
即可,这其实已经很灵活了。
问题
这种简单的 Factory 机制存在两个问题:
- 要更换 Factory,用户必须修改代码,这虽然有点繁琐,但不算致命
- 致命的是:如果要更换的是第三方的 Factory,必须在代码中引入对第三方 Factory 的依赖!
- 如果通过其它语言(例如 Java)使用,还需要专门为第三方依赖实现 binding
当年在 TerarkDB
中,我为了实现无缝集成 TerarkZipTable
,避免用户修改代码,使用了一种非常 Hack 的方案:在 DB::Open
中拦截配置,如果发现了相关的环境变量,就启用 TerarkZipTable
。 这样就允许用户不用修改代码,只需要定义环境变量就能使用 TerarkZipTable
。
这种配置方式实现了 TerarkDB 当时的预期目标,但只是一个简陋的补丁!
作为一个完备的、系统化的解决方案,我们(ToplingDB)期望的插件化,仍以 TableFactory
为例,应该让用户可以这样定义 TableFactory
:
std::string table_factory_class = ReadFromSomeWhere(...);
std::string table_factory_options = ReadFromSomeWhere(...);
Options opt;
opt.table_factory = NewTableFactory(table_factory_class, table_factory_options);
传统插件化方案
要在更换 Factory 时只需要修改配置而不用修改代码,我们需要将相应的配置项(例如类名)映射到 Factory(的基类)对象(的创建函数),这样就需要一个保存了这种映射关系的全局映射表。 仍以 RocksDB
的 TableFactory
为例,现有代码大致这样:
class TableFactory {
public:
virtual Status NewTableReader(...) const = 0;
virtual TableBuilder* NewTableBuilder(...) const = 0;
// more ...
};
TableFactory* NewBlockBasedTableFactory(const BlockBasedTableOptions&);
TableFactory* NewCuckooTableFactory(const CuckooTableOptions&);
TableFactory* NewPlainTableFactory(const PlainTableOptions&);
我们增加一个全局 map
,把类名映射到 NewXXX
函数,但首先就碰到一个问题:这几个函数的 prototype 是不同的,为了统一化,我们把这些 XXXOptions
序列化为 string
:
TableFactory* NewBlockBasedTableFactoryFromString(const std::string&);
TableFactory* NewCuckooTableFactoryFromString(const std::string&);
TableFactory* NewPlainTableFactoryFromString(const std::string&);
现在可以开始下一步了,定义一个全局 map
,并注册这三个 Factory
:
std::map<std::string, TableFactory*(*)(const std::string&)> table_factory_map;
table_factory_map["BlockBasedTable"] = &NewBlockBasedTableFactoryFromString;
table_factory_map["CuckooTable"] = &NewCuckooTableFactoryFromString;
table_factory_map["PlainTable"] = &NewPlainTableFactoryFromString;
大体框架是这样,但是,具体到细节,大致会是这样:
class TableFactory {
public: // 略去不相关代码 ...
using Map = std::map<std::string, TableFactory*(*)(const std::string&)>;
static Map& get_reg_map() { static Map m; return m; }
static TableFactory*
NewTableFactory(const std::string& clazz, const std::string& options) {
return get_reg_map()[clazz](options); // 省略错误检查
}
struct AutoReg {
AutoReg(const std::string& clazz, TableFactory*(*fn)(const std::string&))
{ get_reg_map()[clazz] = fn; }
};
};
#define REGISTER_TABLE_FACTORY(clazz, fn) \
static TableFactory::AutoReg gs_##fn(clazz, &fn)
在某 .cc 文件中的全局作用域(下面三个注册可能分散在每个 Table 各自的 .cc 文件中):
REGISTER_TABLE_FACTORY("BlockBasedTable", NewBlockBasedTableFactoryFromString);
REGISTER_TABLE_FACTORY("CuckooTable", NewCuckooTableFactoryFromString);
REGISTER_TABLE_FACTORY("PlainTable", NewPlainTableFactoryFromString);
前面用户代码的调用处改成这样就可以了:
TableFactory::NewTableFactory(table_factory_class, table_factory_options);
这实际上就是很多成熟系统使用的插件化机制。我们把 AutoReg
放入 TableFactory
类中,作为一个内部类,其原因是为了避免污染外层 namespace, REGISTER_TABLE_FACTORY
用来在全局作用域定义一个 AutoReg
对象,该对象在 main
函数执行之前初始化,定义这么一个宏主要是为了方便、统一化,以及可读性,理论上,不使用 REGISTER_TABLE_FACTORY
而完全手写 AutoReg
也是可以的。
接下来的问题是,RocksDB
有大量这样的 XXXFactory
,对于每个 XXXFactory
,我们都写一套这样的代码,工作量很大,很枯燥,还容易出错。于是我们抽象出一个 Factoryable
的模板类:
template<class Product>
class Factoryable { // Factoryable 位于某个公共头文件如 factoryable.h
using Map = std::map<std::string, Product*(*)(const std::string&)>;
static Map& get_reg_map() { static Map m; return m; }
static Product*
NewProduct(const std::string& clazz, const std::string& params) {
return get_reg_map()[clazz](params); // 省略错误检查
}
struct AutoReg {
AutoReg(const std::string& clazz, Product*(*fn)(const std::string&))
{ get_reg_map()[clazz] = fn; }
};
};
class TableFactory : public Factoryable<TableFactory> {
public:
// 此处的 RocksDB 原有代码不做任何改动
};
#define REGISTER_FACTORY_PRODUCT(clazz, fn) \
static decltype(*fn(std::string())::AutoReg gs_##fn(clazz, &fn)
相应的,前面用户代码的调用处改成这样:
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
:
#define REGISTER_FACTORY_PRODUCT(clazz, fn) \
static Factoryable<decltype(*fn(std::string())>::AutoReg gs_##fn(clazz, &fn)
为了语义上更合逻辑,我们将 Factoryable 重命名为 PluginFactory,再增加一个全局模板函数:
template<class Product>
Product* NewPluginProduct(const std::string& clazz, const std::string& params) {
return PluginFactory<Product>::NewProduct(clazz, params);
}
相应的用户代码就是:
NewPluginProduct<TableFactory>(table_factory_class, table_factory_options);
应用
在 ToplingDB 中,我们使用了这种旁落插件化设计模式,当然,相应的实现代码比这里的 demo 代码要复杂很多。 更进一步,同样是在 ToplingDB 中,我们还支持: * 对象的旁路序列化 * 对象的 REST API 及 Web 可视化展示/修改
这两个功能完整复用了 PluginFactory,只是额外定义了两个模板类,SeDeFunc:
template<class Object> struct SerDeFunc {
virtual ~SerDeFunc() {}
virtual Status Serialize(const Object&, string* output) const = 0;
virtual Status DeSerialize(Object*, const Slice& input) const = 0;
};
template<class Object>
using SerDeFactory = PluginFactory<std::shared_ptr<SerDeFunc<Object> > >;
以及 PluginManipFunc:
template<class Object> struct PluginManipFunc {
virtual ~PluginManipFunc() {} // Repo 指 ConfigRepository
virtual void Update(Object*, const json&, const Repo&) const = 0;
virtual string ToString(const Object&, const json&, const Repo&) const = 0;
};
template<class Object>
using PluginManip = PluginFactory<const PluginManipFunc<Object>*>;