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

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 Tair(兼容Redis),内存型 2GB
简介: 在游戏服务器开发过程中,我们经常会在动手码代码之前好好的设计一番,如何设计类,如何设计接口,如何调用,有没有什么隐患,在这些问题考虑评审可以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(跨平台开发)

相关文章
|
设计模式 算法 Java
设计模式第十五讲:重构 - 改善既有代码的设计(下)
设计模式第十五讲:重构 - 改善既有代码的设计
292 0
|
5月前
|
算法 程序员
代码之舞:从逻辑之美到技术之艺
在数字世界的舞台上,代码不仅仅是冷冰冰的文字序列,而是充满韵律与美感的艺术。本文将带领读者走进编程的世界,探索如何通过逻辑的严谨性与创造性思维的结合,将代码变成一种独特的艺术形式。我们将一同见证技术与艺术如何交织在一起,创造出令人惊叹的作品。
|
监控 小程序 Java
《优化接口设计的思路》系列:第五篇—接口发生异常如何统一处理
大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。
385 0
《优化接口设计的思路》系列:第五篇—接口发生异常如何统一处理
|
设计模式 Java 测试技术
设计模式第十五讲:重构 - 改善既有代码的设计(上)
设计模式第十五讲:重构 - 改善既有代码的设计
333 0
|
设计模式 Java
【Java设计模式 规范与重构】 一 重构的目的、内容、时机、方法
【Java设计模式 规范与重构】 一 重构的目的、内容、时机、方法
196 0
|
编译器 C#
【C#本质论 六】类-从设计的角度去认知(封装)(下)
【C#本质论 六】类-从设计的角度去认知(封装)(下)
91 0
|
存储 Java 程序员
【C#本质论 六】类-从设计的角度去认知(封装)(上)
【C#本质论 六】类-从设计的角度去认知(封装)(上)
112 0
|
设计模式 JSON 缓存
如何“好好利用多态”写出又臭又长又难以维护的代码?| Feeds 流重构方案
如何“好好利用多态”写出又臭又长又难以维护的代码?| Feeds 流重构方案
85 0
|
缓存 负载均衡 Kubernetes
如何设计一个安全的对外接口,老司机总结了这几点
博主之前做过恒丰银行代收付系统(相当于支付接口),包括现在的oltpapi交易接口和虚拟业务的对外提供数据接口。总之,当你做了很多项目写了很多代码的时候,就需要回过头来,多总结总结,这样你会看到更多之前写代码的时候看不到的东西,也能更明白为什么要这样做。
|
Android开发 UED iOS开发
一个淘宝的bug,让我弄懂了它的底层逻辑和顶层设计
一个淘宝的bug,让我弄懂了它的底层逻辑和顶层设计
一个淘宝的bug,让我弄懂了它的底层逻辑和顶层设计