编写此文的目的,并不是教大家如何写出没有Bug的代码。 没有Bug是不可能的。 话虽如此,但是我们完全可以在开发阶段,避免一些低级的Bug。笔者在游戏上线后维护服务器的时间较长,我发现线上所谓的严重问题,真正出问题的代码就是那么几行(一般不会超过10行)。也就是说,这些容易出问题的部分,并不是系统中复杂的部分。因此,我们一定有办法在事前对其进行规避。
我认为避免Bug需要我们有良好的开发习惯,和冷静耐心的心态。于此之上,再通过经验加以一些技巧,就可以避免很多大小Bug了。其中,我认为最重要的是习惯。好的习惯不会让你多花太多时间,但是能尽早发现问题。
习惯和技巧
自测
自测,即代码编写结束后,在本地或开发环境中运行,然后通过白盒或者黑盒的方式测试自己开发的代码。
在本地开发时,务必拥有一个开发环境,这个开发环境可以测试自己正在开发的、还未合并到主干的代码。有了这个环境,你就可以快速地将自己新开发的内容运行起来。如果可以Debug查看每一行的结果就更好了。
自测的意义在于,你可以成为自己代码最早的测试人员,验证自己的思路是否真的被正确开发。自测能给你一个机会,让你执行自己的代码,并且能让以下容易遗漏和犯错的部分得到快速验证:
- 仔细检查if语句的逻辑判断是否正确
- 检查玩家数据状态是否正确改变
- 注销后再登录,即可检查玩家数据是否正确写入
如果你的环境不允许你打断点,或者一些情况比较复杂(多线程环境或有延时函数等),你可以在本地多打一些debug级别的日志,查看数据的处理过程。
Q&A
- 我等客户端同学开发完,联调的时候再一起测不行吗?
可以,但是在联调时你的工作重心已经到了其他地方,你会忽略或遗忘当时写这段代码的一些想法。而这些想法很有可能就是容易出错的地方。另外客户端同学一般只会测自己感兴趣或者担心出问题的部分,他们关注的部分不一定和你一致。
- 要是我都写正确了,自测不就是浪费时间吗?
不。我们人类一定会犯错,即使我们逻辑写对了,也有可能会把诸如道具产销日志等一些不影响功能,但是运营、策划需要的部分遗漏,也有可能会忘记打warning日志或者error日志,或者是发现一些变量名不合适,或者是重新回看的时候发现代码结构冗余臃肿。这些都是值得修改优化的点。如果我们不自测,那么可能未来就没有时间拿来安排自测了。
- 服务器开发逻辑的时候,没有现成可用的客户端包进行测试,自测很麻烦。
是的,很麻烦,但是并非毫无办法,接下来会介绍一些自测的技巧。
自测的技巧
- 对于简单的客户端和服务器交互的接口,可以想办法在客户端手动输入协议号及协议结构,发送指定的协议数据,然后在服务器打Debug断点进行检查。
- 对于玩家数据经常变化的新功能,可以在数据变化推送的时候打印debug日志,通过日志来确认玩家数据正确性。
- 对于情况比较复杂的功能,可以先写一些私下用的一次性gm,先模拟复杂的情况,然后再进行目标功能的测试。、 总之,只要你想测到你的功能,在环境允许的情况下,一定有办法测得到。
变量及函数命名技巧
- 表示某事的次数不要用Times,而是用Count。原因是Times容易和Time混淆,无法一眼区分到底是次数还是时间
- 表示时间长度不要用Time,而是用Duration。因为“时间”这个概念本身就有歧义,可以表示某个时刻,也可以表示某一段时间。建议用Time甚至Timestamp,TimeMillis,TimeSeconds来具体指代某个时刻的时间戳,用duration来指代时间段。
- 如果针对某个对象要设置某个属性的值,并且设置时还要对其有特殊逻辑,并且项目中已经装了lombok依赖,则最好不要再命名setXXX(),因为这样会重写lombok的@Setter省略的函数,并且容易让阅读者忽略这个函数内有特殊逻辑。
不要使用setXXX(),但是你可以针对这个业务逻辑,定义一个更贴合业务的逻辑,比如设置玩家积分时,同时更新排行榜上的玩家数据,则setScore()会让人误以为这个函数只是给对象里的score属性赋值,而如果是叫updateScore()或updateScoreAndRank(),则可以直观的知道,这个函数要更新积分,并且可能会更新排行榜。
代码分段
代码一定要分段写。印象中有一些书籍或者公司里的开发规范中约定,每个函数不要超过多少多少行。我想也许是担心大家把函数写得太长,可读性变差而设的规则。但是,我认为真正的问题在于可读性。在某些特定情况下,一些函数可能来不及优化,不得不写得很长,但我们至少可以通过分段和注释,对代码进行梳理。
一个好的代码分段结构,大概如下:
//单行注释A:介绍接下来一小段要执行什么内容
aaaaa;
bbbbb;
if (c) {
ddd;
.....
}
//单行注释B:介绍接下来一小段要执行什么内容
aaaaa;
bbbbb;
if (c) {
ddd;
.....
}
//...
注意,在两段代码之间,有一个空行。这个空行很重要。他代表着上面这段代码的含义是最上面单行注释A,而下一段代码的含义是单行注释B。如此,阅读者可以很轻松地理解这两段代码到底分别执行了什么逻辑。
分段的技巧
在理清楚需求的业务逻辑后,你可以先写一个空函数,然后再函数内只写注释,不写逻辑。
public void reward() {
//判断功能开启
//判断活动开启
//判断玩家传参
//判断玩家领取状态
//扣除玩家消耗
//修改状态
//发放奖励
//推送奖励弹窗
//返回成功给客户端
}
当你把这个函数需要做的事情梳理清除,简单写出注释,分段也是水到渠成的事情了。
代码注释
代码注释不是越多越好,也不是越长越好,而是越契合团队的理解越好。每个组都有自己的术语表(他们可能没有明确列出,但是他们一定有一套约定俗成的用词),一定要遵守他们的术语,用团队中常用术语进行注释的书写。
写注释不是必须的,写注释是帮助他人和未来的自己理解代码的含义,加快理解代码的速度。因此,如果团队里的其他成员有足够的能力,能直接阅读代码来理解逻辑,那么也可以不用注释。
注释的形式
在需要注释的情况下,可以在以下位置进行注释:
- 一个函数的开头,用/** */多行注释,必要时可以填写@param、@return 介绍参数和返回值
- 一段代码的开头,比如上一节在每一小段的开头,新起一行进行注释,介绍后续这段代码块的逻辑
- 变量比较特殊,或与该变量相关逻辑比较复杂,则在变量声明后,用单行注释来介绍这个变量。
不用注释的情况
- 功能逻辑简单易懂
- 在其他功能里已经非常普遍(如判定玩家等级,判断服务器开启天数等)
- 变量名及函数名足够清晰,已经能够解释功能需求
需要注释的情况
- 命名过长、用词相对生僻,需要用注释进行补充介绍
- 业务逻辑比较特殊,不写注释不好理解(评判标准:不写任何注释的情况下,三个月后的自己重新阅读这段代码,能否回忆起逻辑?如果不能,那么就需要写注释)
- 特殊处理的情况。诸如修复玩家数据的逻辑,并不是正常游戏的业务逻辑,而是版本迭代过程中的修复逻辑,这类情况需要编写注释明确记录日期、问题描述,(如果有问题单号也可以备注一下)等
注释不是越多越好
注释写得多也会带来其他的问题,比如后续需求变更时,我们会因为工作排期紧张,优先修改代码,而忽略了注释。在之后的维护中,可能会使后来的维护者感到疑惑。
注意点
- 当改动代码逻辑的时候,尽可能把注释一起进行修改。如果你来不及,可以放到代码review阶段去做
- 当团队中拥有版本管理工具(git,svn等)时,不要用注释来删除那些“暂时不用,但是未来可能会用”的代码,而是在提交描述里明确记录这一次提交删除了某一部分的代码。未来如果需要找到这部分需要删除的代码,则可以用版本管理工具搜索提交描述的内容,从版本管理工具找到代码。(并且按照经验,被注释掉的代码很可能不会再被打开,而且也没有人敢删除)
改动已有的类时要当心
- 给一个类新增属性时,如果这个类重写了clone(),需要考虑clone()时,这个新属性是否也要进行处理
- 如果这个类重写了equals()和hashCode(),则需要考虑新属性是否要处理,而且两个函数都要处理
- 修改一个函数的业务逻辑,则需要考虑这个类是否是其他类的基类,是否有其他的派生类对这个函数重写,是否影响其他类的逻辑
改动到底层功能的代码时,一定要争取时间自测
在面向对象的设计思想下,必然有一些相似的类,他们有着类似的功能业务逻辑。我们会将通用逻辑抽取到父类,然后子类只针对自身业务,新增一些自己的属性和函数,或是重写一些父类函数。如果修改到了父类的代码,则一定要在代码层面检查一遍,这次修改可能影响到的子类有哪些。如果有足够的条件,最好能自己将影响的子类相关的业务逻辑自测一遍,并且向管理者反馈本次修改的影响范围。
定义枚举或者常量,拒绝使用魔法数字
除了0以外,在代码里的业务逻辑,类型、状态一般是用数字区分,比如道具类型,领奖状态等。一定要定义常量或者枚举来进行业务逻辑的编写,并在常量或枚举定义时介绍其含义。
即使是定义了常量或者枚举,也不要用诸如"TYPE_1",“TYPE_2"这样简单的区分,最好是用有含义并且相对简短的单词来进行编写枚举,比如"TYPE_PLAYER”,“TYPE_ROBOT”,这样阅读代码的人可以一眼知道1对应的是玩家,2对应的是机器人。
和功能相关的所有同事积极沟通,认真听取他们的需求
这里的需求并不是策划案文档里的需求。事实上,在我参与的几个项目组中,每一个功能的策划案都和最终的效果不完全一样。因为需求是不断变化的,而策划案是死的。尤其是游戏这样的复杂逻辑,在管理不规范的团队中,只看着策划案做,一定会丢失一些隐藏的需求,或者误解策划的真正目的。
所以,在研究策划案之后,我们最好能准备一些问题,反过来向文档编写者进行提问,最好能帮他们把逻辑理顺。这一切的目的都是为了我们后续的开发更顺畅,提前将关键的问题找到并且抛出来。
而除了策划,我们还需要和客户端同学进行沟通。设计协议的时候一定要对着UX交互图或者UI渲染图进行设计,定完协议后需要向客户端同学进行介绍,而不是让他们自己理解;有一些需求是客户端处理的需求,但是可能需要服务器在特定条件下发送一些数据,这种情况也需要提前沟通,我们就可以提前做一些准备。
不要认为需求简单就懒得沟通。新做的功能必然有新的要解决的问题。一定要思考这个需求是否解决了他们想解决的问题,而不是只做需求的实现者。
有空的话,看看同事提交的代码
不是只有负责人才需要Review代码。相反,我认为每个人都可以Review代码。不论你的同事能力如何,你总能从别人的代码中获得一些新的理解。如果同事写得好,我们可以学习;同事写得不够好,我们可以提意见。顺便,我们可以知道其他人在这个版本里做了什么功能。如果你是一个团队里的新成员,那么看看别人的提交,阅读他人的提交变更,可以很快学习到这个团队里开发功能的一些惯例。
有空的话,看看服务器日志
功能到线上更新前,一般会在内网先进行更新和测试。此时需要关注服务器的日志,观察最近新发生的报错,或者一些罕见的、异常的日志。测试人员可能因为测试不畅,报了太多错,导致他们无法从游戏客户端界面获取到正确的报错信息,或者因为一些保护逻辑,而修正了某个错误逻辑,而服务器日志可以告诉你具体哪里报错。不要只听测试人员的反馈进行修复Bug,而是结合日志进行分析,找到真正的问题所在。
当服务器更新之后,更要抽出一点时间看服务器的日志。在内网环境下,不一定能完整测试出外网玩家的行为和数据环境。但是外网如果报了内网没有发现的错,则需要仔细分析,提前发现,提前修正错误的情况。如果我们干等客服反馈,有可能会错失修复Bug的最佳时机。因为玩家发现问题,反馈问题,客服记录,客服反馈,到项目组这边,再到项目组评估问题是否存在,这在工作繁忙的时候会浪费大量时间。而如果能提前从日志里发现端倪,提前通过日志排查,则会让影响面缩小很多。
抄写别人的功能时,多长一些心眼
有时候我们会接到某个需求,“做得和已有的XXX差不多就行了”。这时,一定要担心自己在临摹抄写代码时的一些细节,如传参是否正确,变量含义是否和之前代码一致。
如果有条件,最好请“已有的XXX”功能的开发人员帮你Review过一遍。你对代码的理解,大概率和他略有不同。
心态
代码永远会写错,我们能做的就是尽量避免错误
写程序的人或多或少有一种“造物主”的心态,因为在软件的世界里,所有的逻辑都是我们一行一行编写开发,将逻辑实现的。但是我们无法真正做到完美无暇,一次编译就通过,且没有Bug。原因是底层框架可能有所不兼容、自己能力不足或需求理解不清、策划改动需求导致推翻重写、甚至是线上环境出问题导致不可知的Bug等。
保持一个良好的心态,明白我们不管做了多少努力,该犯错还是要犯错。我们要勇敢的面对错误的发生,从错误中吸取教训,不断成长。
另外,并不是所有的Bug都如洪水猛兽。大多数情况下,有些Bug玩家可能忽略、不在意甚至“自适应”了,有些Bug在领导眼里并不重要,所以优先级放得很低。这些情况我们也可以通过一些渠道了解,然后我们也可以动态地调整自己的期待。
出Bug不可怕,逃避Bug才可怕
所有的问题都会解决的。
在经验匮乏的前几年,我时常会因为自己的开发经验不足,而担心更新到线上出问题担惊受怕。这更多是因为读书时遗留的一部分学生思维,认为线上出问题,是我“作业”没做对。这个逻辑在读书时期或许是正确的,但是在实战开发中则是需要割舍的。
因为游戏服务器是一个不断迭代开发的巨大项目,并且工期比传统行业更紧更急(当然出问题的代价也更低),这常常导致在少部分情况下,我们无法对每一次修改进行完整的测试。(是的,即使是我们做到了上述所有的习惯和技巧,也无法每一次都完整测试),所以服务器出问题是一个必然的情况。
我们能做的就是不要逃避Bug。是的,Bug来的时候,我们会不知所措,会很疑惑,甚至会有点沮丧——自己辛苦写的代码还是报错了,并且如果是严重的涉及奖励发放的问题,则可能需要挤占我们的休息时间。
但是,线上出问题,正是一个积攒经验的机会!我们已经尽人事,养成良好习惯,熟悉各种技巧,尽力提前核对并且预防需求变更和未知情况,如果还是报错,那么正是我们还存在未知情况,正是我们提升自我的机会。保持积极,勇敢面对。