少三2后端代码的重构之路
前言
作为程序猿,我们的工作就是编写代码,每个项目对代码质量的要求不一,每个人对代码质量的重视度也不一样,很多人将项目中的代码质量不高归咎于项目组的领导重视度不高。是的,对于项目组来说,大家的关注点都在项目进度及功能完成度上面,至于你的逻辑代码是怎么写的,只有程序猿自己知道,即使是屎一样的代码,只要在规定的时间内完成,最后的运行结果是正确的,其他就都不重要了。所以,越是老项目,越是没有人愿意去维护,我们只负责制造屎山,却不负责清理屎山,久而久之,这座山也就越堆越高了。也许,很多人觉得最好的解决办法是跳槽,然而,很大的概率是:你刚从一座屎山跳了出来,却掉进了另一座屎山。
对于项目组技术管理者,我们就不能仅仅着眼于项目进度和完成度,我们要重视代码质量,否则这座屎山只能像五指山一样压着你不能动弹。那如何避免这样的事情发生呢?第一、重视代码设计,如果在写代码之前就规划好代码结构,那最终编写出的代码应该差不到哪里;第二、重视代码重构,在每次修改代码时,不是一味的以完成任务为目标,而是站在整体代码结构的角度,如果发现代码结构已经无法适应策划需求,那就必须进行代码重构。在大多数项目中,造成屎山越堆越高的最大原因就是:大家都是以最小的改动来修改代码,却不愿意重构代码,最终补丁式代码越来越臃肿。
必须承认,在某些情况下,我们需要在设计和时间之间做妥协,为了运营手段需要尽快上线,而忽略代码设计的情况确实存在。但我们要对这些历史欠债做到心中有数,在版本节奏缓下来的时候通过代码重构把这些债还上。最近给少三2后端同学布置一个作业,把自己在项目中看到的问题做一次盘点,然后做一次还债,在一人一杯奶茶的动力下,以下是这次集体还债的战果。
重构之路
1. 抽奖逻辑优化(by 阿柒)
优化思路
做风华鉴功能时简单优化了抽奖逻辑,主要有两点:
- 随机次数优化,原来for循环中每次都有重新执行SelectOneInSection方法,有性能消耗;
- 奖池仅支持int为key的map,后续扩展比较麻烦,使用interface{}作为奖池唯一key,可以支持各种奖池配置;
代码设计
2. GM指令处理优化(by 零下)
优化思路
- 目前游戏的GM指令都是diplomat通过同一个grpc请求转发给游戏服;
- 场景下收到Grpc请求后通过handleGrpcMessage方法,switch data.Cmd来处理,导致所有的gm指令的处理耦合在一起,一个fucntion的代码量越来越大,改动起来不方便;
- 优化的方案可以参考协议处理的方案,给不同的data.Cmd注册不同的command,每个command处理自己的业务逻辑,这样每条GM指令可以做到解耦,修改起来互不影响;
代码设计
- 新增grpc_command.go 文件
- Scene下新增GrpcCmd字段
- 新增一条grpc处理指令
- 原handleGrpcMessage方法修改后
3. 客户端协议回包优化(by 追光)
优化思路
- 游戏服每条协议的处理逻辑Process方法,都有大量的拦截逻辑或者回包逻辑,逻辑比较繁琐,经常会遗漏回包,甚至用错msgId的情况;
- 考虑将 Process(*Scene, zebra.Session, *zebra.Message) bool 方法的返回值改成结构体;
- 将原有的异常逻辑时return false改为return nil,无需回包但是return true的地方,返回特殊的无回包结构体;
- 对客户端的回包统一在底层Dispatch时处理;
代码设计
4. 功能解锁优化(by 神风烈士提尔)
优化思路
- 当前功能解锁逻辑是在处理消息逻辑的开始部分处理,每条客户端消息都需要单独处理,代码冗余,而且容易遗漏;
- 在底层消息分发函数中,已经存在部分玩家相关逻辑,用于客户端消息断线重连时使用,如果将检测功能解锁的代码放入消息分发的函数内处理,可以减少大量的冗余代码,并且减少遗漏代码的可能;
代码设计
- 当前的消息处理方式:
- 新的消息处理方式:
5. 配置文件检查机制优化(by 王小石)
优化思路
- 目前配置文件检查只能在当前配置文件内部,无法关联配置表检查;
- 将配置表检查逻辑放到配置文件加载之后,并把配置文件管理器指针传递到检查函数Check方法内,即可实现关联配置表检查;
代码设计
- 当前配置检查逻辑
- 修改后的配置检查逻辑
6. 邮件系统重构(by 东东)
左侧为当前邮件系统的邮件结构,右侧为优化后的邮件结构,具体优化有如下几点:
a. 抽象出MailBase结构,供系统邮件(SystemMail)和个人邮件(PersonMail)复用;
b. 除了个人邮件(PersonMail)之外,新定义一个系统邮件(SystemMail)结构,用于保存GM系统群发的邮件,这类邮件有着共同的邮件内容,只是接受人不同,可能是一个收件人列表,也可能是一些筛选条件,符合条件的玩家都可以收到这份邮件;
c. 取消语音相关的字段,配置一条通用的语音邮件配置,通过动态参数(params)存储相关内容,用于客户端解析;
d. 取消gotAward字段,复用status字段表示,如果是存在附件的邮件,领奖状态使用已读状态表示,这样玩家未领取的奖励会一直提示为新邮件(对玩家更友好);
7. 任务系统重构(by 东东/王小石)
当前任务系统结构图
新通用任务系统结构图
优化内容:
a. 将事件触发器Action改名为EventTrigger更有意义,ExecuteHandler改名为Trigger亦同,并在Manager中增加了部分EventHandler的处理方法;
b. 事件处理器EventHandler设计更合理,增加事件记录器EventRecord及任务检查器RequirementChecker相关的方法支持,事件处理可以使用通用的CommonEventHandler,也可以独立设计私有的EventHandler;
c. 抽象出任务检查器RequirementChecker,主要负责任务进度处理与任务完成情况检查;
d. 抽象出事件记录器EventRecorder,主要负责任务进度记录存储,支持查询,保存及客户端数据同步,有统一的玩家事件记录管理器UserEventRecorderManager进行管理;
e. 抽象出状态类任务条件RequirementCondition及进度类任务条件RequirementProgressCondition,主要负责任务的条件检查,支持结合统一的配置条件与事件记录器进行条件检查;
8. 战斗服战斗逻辑线程模型重构(by 王小石)
优化思路
- 当前战斗服中战斗逻辑线程是根据业务逻辑实际需要从线程池中获取,使用一个自增ID,确保能够平均分配给所有战斗逻辑线程,并且开始只会创建一个战斗逻辑线程,并根据后续请求动态创建战斗逻辑线程;
- 这样的设计存在一个缺陷,就是消息分发器无法判断当前战斗线程池中的负载状态,在战斗代码正常的情况下,这样的设计没有问题,但如果战斗逻辑存在不平衡,有的战斗时间较长,有的战斗时间较短,甚至有可能存在低概率的死循环逻辑,造成部分战斗代码卡死,则会影响所有玩家的战斗体验,造成大面积的玩家战斗卡死的情况;
- 优化思路为在战斗管理器中,增加一个战斗消息队列(channel),战斗逻辑线程则按照配置数量全部启动,而且是主动接受战斗消息队列的战斗消息,如果有空闲就处理战斗消息,如果一直忙碌则不会再接收战斗消息,即使是低概率的死循环逻辑,影响的仅仅是个别玩家,避免出现大面积交叉感染的情况;
代码设计
-
战斗管理器AsyncBattleManager中增加一个战斗消息*BattleMessage的channel用作战斗消息队列AsyncBattleQueue。
-
在Init()时启动足够的战斗逻辑线程池,并在收到网络传输的战斗消息时,将战斗消息插入消息队列AsyncBattleQueue。
-
在战斗逻辑线程AsyncBattleUnit中,每个时钟周期都会主动从战斗管理器的消息队列AsyncBattleQueue中读取战斗消息。
结束语
综合看少三2后端的代码,其实是很优秀的,因为我们会定期重构,这样代码才会越来越优秀。并不是我们很闲,只是因为我们对自己要求更高,我们想要做的更好,只有精益求精了,我们才能做出更好的项目来。这些优化有平时做业务时顺手做的优化,也有的只是一个设计思路,更有的已经在新项目中已经重构。所以,这只是一次代码秀,让我们这些平时专注专研技术的程序猿们得到一次曝光的机会,让大家了解到我们的少年程序猿都是很优秀的,也期待优秀的你的加入。