在游戏服务器开发过程中,我们经常会在动手码代码之前好好的设计一番,如何设计类,如何设计接口,如何调用,有没有什么隐患,在这些问题考虑评审可以Cover现阶段的需求的情况下再动手。
不过,对于一些初级,甚至中高级开发者,仍然不可避免的进入了一个死胡同,缺少设计,屎山代码堆积,越堆越臭,越写越烂,直到很难维护必须要重新改造。
最近我给M部门面试服务器主程序开发的职位,我不问开发语言的语法,我只问他们的架构设计经验,我发现相当一部分5-12年“本应该有足够开发经验”的开发组长,或开发主程序缺乏设计,缺乏容错,缺乏创新,比如一些服务器宕机如何崩溃拉起恢复玩家数据,数据库的异步线程读写如何避免被其他线程写回呢,至少目前能听到合理方案的面试者的回答不多,这也是我想写这篇文章的出发点,以此来分享给大家,如何去屎山代码。
这一节我来讲下常见的一种屎山代码,就是接口不够抽象化,虽然有面向对象的思想,但是意识还不够再抽象,导致代码堆积过于沉重,对后边的开发者也造成大量的重复工作。这里我罗列以一个服务器中常用的例子:
目录
初级开发者
游戏服务器启动过程中一般都需要加载服务器端的配置,比如玩家等级,玩家英雄,技能,装备道具,活动,邮件等配置,随着游戏功能的不断迭代,配置也越来越大越来越多,初级开发者常见的写法是这样的:
有一个玩家等级配置,就写一个LevelCfg的类来加载配置
class LevelCfg { private: string filepath_; public: LevelCfg(string filepath) :filepath_(filepath) {} bool LoadCfg() { std::ifstream ifs(filepath_); if (!ifs.is_open()) { cout << "open file failed" << filepath_; return false; } std::string str((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>()); /* json 转 protobuf。 */ if (!ParseJson(str)) { cout << "json to protobuffer failed file :" << filepath_; return false; } ifs.close(); return true; } bool ParseJson(const string& str) { //解析反序列化到LevelCfg对象 } };
还有一个邮件类的配置,再写一个MailCfg的类,包含加载配置文件和解析的方法:
class MailCfg { private: string filepath_; public: MailCfg(string filepath) :filepath_(filepath) {} bool LoadCfg() { std::ifstream ifs(filepath_); if (!ifs.is_open()) { cout << "open file failed" << filepath_; return false; } std::string str((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>()); /* json 转 protobuf。 */ if (!ParseJson(str)) { cout << "json to protobuffer failed file :" << filepath_; return false; } ifs.close(); return true; } bool ParseJson(const string & str) { //解析反序列化到MailCfg对象 } };
然后到了真正调用的时候是这样写的:
int main() { std::cout << "Start Gameserver!\n"; MailCfg mailCfg("mail.json"); LevelCfg levelCfg("level.json"); if (!mailCfg.LoadCfg()) { cout << "failed to load mailcfg"; return -1; } if (!levelCfg.LoadCfg()) { cout << "failed to load levelCfg"; return -1; } while (1) { //TODO //... } }
随着配置文件越来越多,屎山代码开始散发气味,一行又一行的if else,一行又一行的重复性代码让人痛苦不堪。。。
中级开发者
作为高级开发者, 已经发现了不管是什么配置文件类,他们都需要加载配置文件的方法,而且内部都需要通过读文件来解析,所以,是不是可以将加载配置文件的方法抽象出一个接口,所有的继承抽象类的对象来实现相应的解析逻辑。
编辑
所以抽象出ICfg类,一个文件路径的成员变量,因为所有配置文件都需要加载,所以这里就提出来一个加载类方法LoadCfg,只不过内部解析的实现上,各个配置文件又有所差异,因此,我们会给每个继承类提供抽象出一个抽象方法解析配置文件类
class ICfg { private: string filepath_; public: ICfg(string filepath) :filepath_(filepath) {} std::string& GetPath() { return filepath_; } virtual bool LoadCfg() { std::ifstream ifs(filepath_); if (!ifs.is_open()) { cout << "open file failed" << filepath_; return false; } std::string str((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>()); /* json 转 protobuf。 */ if (!ParseCfg(str)) { cout << "json to protobuffer failed file :" << filepath_; return false; } ifs.close(); return true; } private: virtual bool ParseCfg(const string& buffer) = 0; };
此时,我们的等级配置类和邮件配置类的写法也简单了一些,我们来看下:
class MailCfg :public ICfg { public: MailCfg(string filepath) :ICfg(filepath) {} public: bool LoadCfg() { return ICfg::LoadCfg(); } private: virtual bool ParseCfg(const string & str) { //解析反序列化到MailCfg对象 cout << "parse mail cfg ok" << endl; return true; } }; class LevelCfg :public ICfg { public: LevelCfg(string filepath) :ICfg(filepath) {} public: bool LoadCfg() { return ICfg::LoadCfg(); } private: virtual bool ParseCfg(const string& str) { //解析反序列化到LevelCfg对象 cout << "parse level cfg ok" << endl; return true; } };
少了成员变量filepath_,也少了重复性代码读文件再加载解析的逻辑,那么我们对配置文件对象的创建和调用也简单了一些,可以这样来new对象实现多态,并通过调用抽象类接口来实现子类方法。
ICfg* mailCfg = new MailCfg("mail.json"); if (!mailCfg->LoadCfg()) { cout << "failed to load mailcfg"; return -1; }
不过我们发现屎味虽然淡了,但是仍然存在,你会发现在你初始化各种类型的配置文件的时候,以及加载的时候,痛苦不堪,到最后你的代码可能是这样的:
ICfg* mailCfg = new MailCfg("mail.json"); ICfg* levelCfg = new LevelCfg("level.json"); ICfg* activityCfg = new ActivityCfg("activity.json"); ICfg* itemCfg = new ItemCfg("items.json"); ICfg* achievementCfg = new AchievementCfg("achievements.json"); //TODO ..... 其他配置文件初始化创建 if (!mailCfg->LoadCfg()) { cout << "failed to load mailcfg"; return -1; } if (!levelCfg->LoadCfg()) { cout << "failed to load levelCfg"; return -1; } if (!activityCfg->LoadCfg()) { cout << "failed to load activityCfg"; return -1; } if (!itemCfg->LoadCfg()) { cout << "failed to load activityCfg"; return -1; } if (!achievementCfg->LoadCfg()) { cout << "failed to load activityCfg"; return -1; } //TODO ..... 其他配置文件加载解析
不好意思,一个函数就写了四五百行的逻辑,别人翻看代码分了好几页,维护越来越复杂。
高级开发者
针对上面遗留的屎味,我们继续优化改造,我们可以提取一个加载管理配置文件的类,暂且叫CfgManager类,这个类需要来控制管理所有配置文件的添加,加载,释放配置的过程,同时需要提供接口来方便访问不同类的结构,因此它的写法较为简单,如下:
class CfgManager { public: CfgManager() {} virtual ~CfgManager() {} static CfgManager& getInstance() { static CfgManager gameConfig; return gameConfig; } bool AddCfg(ICfg* cfg); bool Load(); ICfg* GetCfg(string fileName); private: list<ICfg*> cfgs_; unordered_map<string, ICfg*> hash_cfgs_; };
其中cfgs_存储不通类型的配置类,hash_cfgs_主要是方便根据配置名查找对应的配置类结构,这里做了映射,将ICfg指针指向cfgs_里 ,使用单例模式来加载,初始化所有的配置,所有的配置类对象将通过AddCfg接口传入进来,所以对象的创建通过外部引入,对象的销毁也可以放到CfgManager的外部来销毁,whatever,你都可以灵活选择。下面是CfgManager的实现:
bool CfgManager::AddCfg(ICfg* config) { auto it = hash_cfgs_.find(config->GetPath()); if (it != hash_cfgs_.end()) { return false; } hash_cfgs_.insert(make_pair(config->GetPath(), config)); cfgs_.push_back(config); return true; } bool CfgManager::Load() { for (auto it : cfgs_) { if (!it->LoadCfg()) { cout << "try load failed" << it->GetPath(); return false; } } return true; } ICfg* CfgManager::GetCfg(string fileName) { auto it = hash_cfgs_.find(fileName); if (it != hash_cfgs_.end()) { return it->second; } return NULL; }
这么一改造,是不是代码瞬间看着漂亮了,而且简洁了:
int main() { std::cout << "Start Gameserver!\n"; CfgManager::GetInstance().AddCfg(new MailCfg("mail.json")); CfgManager::GetInstance().AddCfg(new LevelCfg("level.json")); CfgManager::GetInstance().Load(); while (1) { //TODO //... } }
对于开发者,如果有新的配置类需要解析加载了,你只需要定义配置类的结构和解析规则,并且添加到CfgManager里即可,原来几百行代码的事情,现在只需要这么简单的十几行逻辑即可添加。
这就是抽象接口的优势,关于抽象接口的用法,其实大家可以参考上一篇文章C++库封装mongodb(跨平台开发)