变形记---抽象接口,屎山烂代码如何改造成优质漂亮的代码

本文涉及的产品
云原生数据库 PolarDB MySQL 版,Serverless 5000PCU 100GB
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
简介: 在游戏服务器开发过程中,我们经常会在动手码代码之前好好的设计一番,如何设计类,如何设计接口,如何调用,有没有什么隐患,在这些问题考虑评审可以Cover现阶段的需求的情况下再动手。不过,对于一些初级,甚至中高级开发者,仍然不可避免的进入了一个死胡同,缺少设计,屎山代码堆积,越堆越臭,越写越烂,直到很难维护必须要重新改造。最近我给M部门面试服务器主程序开发的职位,我不问开发语言的语法,我只问他们的架构设计经验,我发现相当一部分5-12年“本应该有足够开发经验。

         在游戏服务器开发过程中,我们经常会在动手码代码之前好好的设计一番,如何设计类,如何设计接口,如何调用,有没有什么隐患,在这些问题考虑评审可以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对象
  }
};

image.gif

       还有一个邮件类的配置,再写一个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对象
  }
};

image.gif

然后到了真正调用的时候是这样写的:

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
    //...
  }
}

image.gif

随着配置文件越来越多,屎山代码开始散发气味,一行又一行的if else,一行又一行的重复性代码让人痛苦不堪。。。

中级开发者  

       作为高级开发者, 已经发现了不管是什么配置文件类,他们都需要加载配置文件的方法,而且内部都需要通过读文件来解析,所以,是不是可以将加载配置文件的方法抽象出一个接口,所有的继承抽象类的对象来实现相应的解析逻辑。

image.gif编辑

  所以抽象出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;
};

image.gif

此时,我们的等级配置类和邮件配置类的写法也简单了一些,我们来看下:

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;
  }
};

image.gif

少了成员变量filepath_,也少了重复性代码读文件再加载解析的逻辑,那么我们对配置文件对象的创建和调用也简单了一些,可以这样来new对象实现多态,并通过调用抽象类接口来实现子类方法。

ICfg* mailCfg = new MailCfg("mail.json"); 
if (!mailCfg->LoadCfg()) {
        cout << "failed to load mailcfg";
        return -1;
}

image.gif

不过我们发现屎味虽然淡了,但是仍然存在,你会发现在你初始化各种类型的配置文件的时候,以及加载的时候,痛苦不堪,到最后你的代码可能是这样的:

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  ..... 其他配置文件加载解析

image.gif

不好意思,一个函数就写了四五百行的逻辑,别人翻看代码分了好几页,维护越来越复杂。

高级开发者

 针对上面遗留的屎味,我们继续优化改造,我们可以提取一个加载管理配置文件的类,暂且叫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_;
};

image.gif

其中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;
}

image.gif

这么一改造,是不是代码瞬间看着漂亮了,而且简洁了:

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
    //...
  }
}

image.gif

对于开发者,如果有新的配置类需要解析加载了,你只需要定义配置类的结构和解析规则,并且添加到CfgManager里即可,原来几百行代码的事情,现在只需要这么简单的十几行逻辑即可添加。

      这就是抽象接口的优势,关于抽象接口的用法,其实大家可以参考上一篇文章C++库封装mongodb(跨平台开发)

相关文章
|
24天前
|
设计模式 Java API
重构旧代码的秘诀:用设计模式 - 适配器模式(Adapter)给Java项目带来新生
【4月更文挑战第7天】适配器模式是解决接口不兼容问题的结构型设计模式,通过引入适配器类实现目标接口并持有不兼容类引用,实现旧代码与新接口的协作。适用于处理兼容性问题、整合遗留代码和集成第三方库。应用时,识别不兼容接口,创建适配器类转换方法调用,然后替换原有引用。注意保持适配器简单、使用组合和考虑扩展性。过度使用可能导致系统复杂和维护成本增加,应谨慎使用。
|
7月前
|
设计模式 算法 Java
设计模式第十五讲:重构 - 改善既有代码的设计(下)
设计模式第十五讲:重构 - 改善既有代码的设计
239 0
|
5月前
|
设计模式 存储 Java
将简单工厂模式改造应用到项目中,而不是纸上谈兵
将简单工厂模式改造应用到项目中,而不是纸上谈兵
44 0
|
7月前
|
监控 小程序 Java
《优化接口设计的思路》系列:第五篇—接口发生异常如何统一处理
大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。
210 0
《优化接口设计的思路》系列:第五篇—接口发生异常如何统一处理
|
7月前
|
设计模式 Java 测试技术
设计模式第十五讲:重构 - 改善既有代码的设计(上)
设计模式第十五讲:重构 - 改善既有代码的设计
259 0
|
7月前
|
vr&ar 安全 AndFix
Metaforce佛萨奇系统开发案例详细丨方案逻辑丨项目程序丨规则玩法丨源码功能
Requirement analysis: Communicate fully with customers to understand their specific needs and expectations for the Metaforce Sasage system, including game types, features, art styles, etc
|
11月前
|
设计模式 Java
【Java设计模式 规范与重构】 一 重构的目的、内容、时机、方法
【Java设计模式 规范与重构】 一 重构的目的、内容、时机、方法
121 0
|
12月前
|
设计模式 JSON 缓存
如何“好好利用多态”写出又臭又长又难以维护的代码?| Feeds 流重构方案
如何“好好利用多态”写出又臭又长又难以维护的代码?| Feeds 流重构方案
58 0
|
12月前
|
缓存 负载均衡 Kubernetes
如何设计一个安全的对外接口,老司机总结了这几点
博主之前做过恒丰银行代收付系统(相当于支付接口),包括现在的oltpapi交易接口和虚拟业务的对外提供数据接口。总之,当你做了很多项目写了很多代码的时候,就需要回过头来,多总结总结,这样你会看到更多之前写代码的时候看不到的东西,也能更明白为什么要这样做。
|
前端开发 算法 数据处理
前端基础向~从项目出手封装工具函数
前端基础向~从项目出手封装工具函数
139 0