配置系统 配置系统有什么用?
我的理解是方便程序的运行和发布。把配置变量都抽离出来放在配置文件中,如果要修改配置变量,就直接在配置文件里修改,然后重新运行程序就可以了。如果没有配置系统的情况下要修改配置变量,一般都是直接改程序源代码,然后重新编译连接,毫无疑问这将会是费时费力的(找对应版本的各种库,对应版本的编译器等等,还要等待漫长的编译连接过程。。。),对于那些非开源软件,想改源代码就更不可能了。。。。
配置系统就能够很好地解决这些问题。
YAML 选择一种用于配置文件的语言,我选的是 YAML。它是专门用来写配置文件的语言,非常简洁和强大,远比 JSON 格式方便。
YAML 实质上是一种通用的数据串行化格式。它的基本语法规则如下:
大小写敏感
使用缩进表示层级关系
缩进时不允许使用 Tab 键,只允许使用空格。
缩进的空格数目不重要,只要相同层级的元素左侧对齐即可
#
表示注释,从这个字符一直到行尾,都会被解析器忽略。
YAML 支持的数据结构有三种:
对象:键值对的集合,又称为映射(mapping)/ 哈希(hashes) / 字典(dictionary)
数组:一组按次序排列的值,又称为序列(sequence) / 列表(list)
纯量(scalars):单个的、不可再分的值
和 JSON 对比,它数据类型更单调简单(JSON 有 6 种类型)
YAML 下载与安装:
yaml-cpp: github repo
mkdir build && cd build && cmake .. && make install
基于 YAML 实现 配置系统 配置系统的原则:
约定优于配置: 约定即源代码中写死的值,而配置是指在配置文件 (.yaml) 中指定的值。
不能无中生有: 在源文件中未定义的配置变量,即使在配置文件 (.yaml) 中定义了也不会生效。
总体的结构是这样的:
由于配置变量一般都由:变量名,变量值,变量描述构成。因此可以抽一个基类出来存放这些共通的属性,必然的,有时候会需要把配置变量输出到控制台给用户看,或根据字符串来重置变量值,因此还需要一个 fromString 和 toString 方法:
1 2 3 4 5 6 7 8 9 class ConfigVarBase {public : using ptr = std ::shared_ptr <ConfigVarBase>; virtual bool fromString (std ::string str) ; virtual void toString () ; private : std ::string _name; std ::string _description; };
其中变量名和变量描述由于类型固定,可以放在基类中,而变量值则不固定了,它可以是任意类型,因此就可以根据基类派生出一个模板子类来表示具体的配置变量:
1 2 3 4 5 6 7 8 template <class T >class ConfigVar : public ConfigVarBase {public : bool fromString (std ::string str) override ; std ::string toString () override ; private : T _val; };
现在有配置变量了,缺一个管理这些配置变量的类,我使用 map 来进行管理
1 2 3 4 5 6 7 8 class Config {public : ... static std::map<std::string, ConfigVarBase::ptr>& GetConfigVars() { static std ::map <std ::string , ConfigVarBase::ptr> g_configVars; return g_configVars; } };
这里为什么要用 static 函数来返回一个 local static 变量 map 呢?这是因为,配置系统可以被其他编译单元内的数据结构使用,如果其他编译单元想要使用 g_configVars 时,它还没有初始化完毕就会产生 runtime error,这种情况就是所谓的 non-local static 初始化顺序不一致。可以使用 local static 的方式来解决,也就是让别的编译单元通过调用函数的方式获取 g_configVars ,这样使用它之前肯定被初始化好了。
这样一来 约定的变量 就实现了!
接下来就是怎么实现,从配置文件 (.yaml) 中读取配置变量。yaml-cpp 库提供了 LoadFile 函数,能从 .yaml 文件中读取 YAML::Node。
由于 .yaml 中的格式和我源代码中变量名字的格式是不一样的:
yaml 中是:
源文件中的变量名则为:A.B = 10,A.C = 20
因此这里需要一个从 YAML 名称格式到 源代码中的变量名称格式的转换。可以借助 yaml-cpp 中的 IsNull, IsScalar, IsSequence, Ismap
对 node 进行递归解析,然后将变量名进行转换。只有对象类型才需要递归解析下去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 void listAllNodes (const std ::string & name, const YAML::Node& node, std ::vector <std ::pair <std ::string , YAML::Node>>& allNodes) { allNodes.push_back(std ::make_pair (name, node)); if (node.IsNull()) { } else if (node.IsScalar()) { } else if (node.IsSequence()) { } else if (node.IsMap()) { for (auto it = node.begin(); it != node.end(); ++it) { listAllNodes(name.empty() ? it->first.as<std ::string >() : name + "." + it->first.Scalar(), it->second, allNodes); } } } void Config::loadFromYaml (const char * filename) { YAML::Node node = YAML::LoadFile(filename); std ::vector <std ::pair <std ::string , YAML::Node>> allNodes; listAllNodes("" , node, allNodes); for (auto i : allNodes) { std ::string name = i.first; if (name.empty()) continue ; ConfigVarBase::ptr p = Config::find(name); if (p) { std ::stringstream ss; ss << i.second; p->fromString(ss.str()); } } }
我们都知道,YAML 文件中仅仅是一些文本,而我们需要根据 YAML 文本得到内存中的对象;或者根据内存中的对象得到 YAML 文本;这本质就是序列化和反序列化。和 JSON 的十分相似。 我做出了这样的总结:
YAML::Node –> std::string –> Type
通过 std::stringstream 来实现 YAML::Node –> std::string. 这一部分 YAML 库已经做好了
通过 自己实现的 LexicalCast\<F, T\>
来做 std::string –> Type 的转换
Type –> std::string –> YAML::Node
通过 YAML::Load 来实现 std::string –> YAML::Node. 这一部分 YAML 库已经做好了
通过 自己实现的 LexicalCast\<F, T\>
来做 Type –> std::string 的转换
fromStr 和 toStr 的实现 对于普通的内置类型可以用 boost::lexical_cast 来实现,而对于复杂的数据类型,例如:vector,list,set,map,unordered_set,unordered_map, 自定义类型 等,就要自己去实现了。
STL 类型的支持:
可以实现一个 LexicalCast 模板类,然后根据具体的 STL 容器对 LexicalCast 进行偏特化就行了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 template <class F , class T >class LexicalCast {public : T operator () (const F& val) { return boost::lexical_cast<T>(val); } }; template <class T >class LexicalCast <std ::string , std ::vector <T>> {public : std ::vector <T> operator () (const std ::string & str) { ... } }; template <class T >class LexicalCast <std ::vector <T>, std ::string > {public : std ::string operator () (const std ::vector <T>& v) { ... } }; .... template <class T , class FromStr = LexicalCast<std ::string , T>, class ToStr = LexicalCast<T, std ::string >> class ConfigVar : public ConfigVarBase {public : bool fromString (std ::string str) override { _val = FromStr()(str); return true ; } std ::string toString () override { return ToStr()(_val); } private : T _val; };
自定义类型的支持:
自定义类型,需要实现 LexicalCast 偏特化,实现后,就可以支持 Config 解析自定义类型,自定义类型可以和常规 STL 容器一起使用。
例如,增加 Person 类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class Person {public : std ::string name; int age; bool sex; }; template <>class LexicalCast <std ::string , Person> {public : Person operator () (const std ::string & str) { YAML::Node node = YAML::Load(str); Person p; p.name = node["name" ].as<std ::string >(); p.age = node["age" ].as<int >(); p.sex = node["sex" ].as<bool >(); return p; } }; template <>class LexicalCast < Person, std ::string > {public : std ::string operator () (const Person& p) { YAML::Node node; node["name" ] = p.name; node["age" ] = p.age; node["sex" ] = p.sex; std ::stringstream ss; ss << node; return ss.str(); } };
配置的事件机制 当一个配置项发生修改的时候,可以反向通知对应的代码。
这个其实挺容易实现的,在 ConfigVar 模板类中添加一个 OnChangeCallBack _cb 回调,它是
std::function<const T& oldVal, const T& newVal> 类型的,每当要改变 ConfigVar::_val 时,先判断一下,新的值是否与旧值不同,如果是的化则回调 _cb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class ConfigVar { ... T getValue () const { return _val; } void setValue (const T& v) { if (v == _val) return ; if (_cb) _cb(_val, v); _val = v; } void setOnChangeCallBack (OnChangeCallBack cb) { _cb = cb; } void delOnChangeCallBack () { _cb = nullptr ; } private : T _val; OnChangeCallBack _cb; };
这样配置系统基本就完成了!