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

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 在游戏服务器开发过程中,我们经常会在动手码代码之前好好的设计一番,如何设计类,如何设计接口,如何调用,有没有什么隐患,在这些问题考虑评审可以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(跨平台开发)

相关文章
|
3月前
|
机器学习/深度学习 算法 调度
【EI复现】基于深度强化学习的微能源网能量管理与优化策略研究(Python代码实现)
【EI复现】基于深度强化学习的微能源网能量管理与优化策略研究(Python代码实现)
194 0
|
安全 Linux iOS开发
Anaconda下载及安装保姆级教程(详细图文)
Anaconda下载及安装保姆级教程(详细图文)
34605 1
Anaconda下载及安装保姆级教程(详细图文)
|
8月前
|
SQL 存储 关系型数据库
简单聊聊MySQL的三大日志(Redo Log、Binlog和Undo Log)各有什么区别
在MySQL数据库管理中,理解Redo Log(重做日志)、Binlog(二进制日志)和Undo Log(回滚日志)至关重要。Redo Log确保数据持久性和崩溃恢复;Binlog用于主从复制和数据恢复,记录逻辑操作;Undo Log支持事务的原子性和隔离性,实现回滚与MVCC。三者协同工作,保障事务ACID特性。文章还详细解析了日志写入流程及可能的异常情况,帮助深入理解数据库日志机制。
1051 0
|
Java 数据库连接 数据库
如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面
本文介绍了如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面。通过合理配置初始连接数、最大连接数和空闲连接超时时间,确保系统性能和稳定性。文章还探讨了同步阻塞、异步回调和信号量等并发控制策略,并提供了异常处理的最佳实践。最后,给出了一个简单的连接池示例代码,并推荐使用成熟的连接池框架(如HikariCP、C3P0)以简化开发。
316 2
|
11月前
|
机器学习/深度学习 存储 C++
《解析 MXNet 的 C++版本在分布式训练中的机遇与挑战》
MXNet C++版本在分布式训练中展现出高效计算性能、灵活跨平台支持和良好可扩展性的优势,但也面临环境配置复杂、通信开销与同步延迟及调试难度大的挑战。深入研究这些优劣,有助于推动深度学习技术在分布式场景下的高效应用。
139 10
|
安全 关系型数据库 MySQL
【赵渝强老师】MySQL的连接方式
本文介绍了MySQL数据库服务器启动后的三种连接方式:本地连接、远程连接和安全连接。详细步骤包括使用root用户登录、修改密码、创建新用户、授权及配置SSL等。并附有视频讲解,帮助读者更好地理解和操作。
1138 1
|
Ubuntu Linux
内核实验(四):Qemu调试Linux内核,实现NFS挂载
本文介绍了在Qemu虚拟机中配置NFS挂载的过程,包括服务端的NFS服务器安装、配置和启动,客户端的DHCP脚本添加和开机脚本修改,以及在Qemu中挂载NFS、测试连通性和解决挂载失败的方法。
906 0
内核实验(四):Qemu调试Linux内核,实现NFS挂载
|
安全 编译器 API
Qt实用小技巧:消除警告
Qt实用小技巧:消除警告
679 1
|
安全 Java 数据库
Spring Boot 3 + JWT + Security 联手打造安全帝国:一篇文章让你掌握未来!
`Spring Security`已经成为`java`后台权限校验的第一选择.今天就通过读代码的方式带大家深入了解一下Security,本文主要是基于开源项目[spring-boot-3-jwt-security](https://github.com/ali-bouali/spring-boot-3-jwt-security)来讲解Spring Security + JWT(Json Web Token).实现用户鉴权,以及权限校验. 所有代码基于`jdk17+`构建.现在让我们开始吧!
3648 1
 Spring Boot 3 + JWT + Security 联手打造安全帝国:一篇文章让你掌握未来!
下一篇
oss云网关配置