2019年04月18日星期五,最晚的一次下班打卡时间23点59分。这么晚下班并不是制度要求,而是我对自己不负责任的惩罚,冷静了一下,重新补充了单元测试。一个项目从开始前后端分离,就对前后端的技能有了更严格的要求。一个看似很简单的用户维护接口,我没有做到各种边界情况的充分考虑,没有维护单元测试代码,造成和前端对接过程中反复出现一些很低级的错误,影响了前端的对接进度,我很抱歉。在此检讨,时刻警醒!
很幸运目前所在的部门实行弹性上班制度,朝九晚五的工作制度在互联网部门中应该是属于为数不多见的。
为什么我的代码会有BUG
总想为BUG找借口
BUG都是有原因的,虽然落地实现的代码是自己写的,但是总会找到一些借口,比如需求不清晰,设计不合理,历史遗留问题等导致的。但是归根结底,是因为自己没有独立思考导致的。开发不仅仅是实现功能,而且还要把不清晰的需求变清晰,不合理的设计变合理,把历史遗留问题逐渐改正的一个过程。
案例
一个很简单的用户维护功能
原型
根据原型设计完成用户的增删改查功能
这个原型只有3个页面:用户查询列表,用户详情页面,添加用户页面。
在用户列表
的页面上,原型所呈现出来的信息:
- 查询的用户包含禁用和启用状态的用户。
- 查询的用户排除已经被删除的用户。(虽然没有标注但是正常逻辑应该如此,除非这是一个比较特殊的平台)
- 用户所关联的平台已经被禁用或者删除,这过滤掉该用户。(虽然没有标注但是正常逻辑应该如此,除非这是一个比较特殊的平台)
在添加用户
的页面上,原型标注了限制条件:
- 用户昵称不能大于5个中文字符,不能大于10个英文字符
- 账号不能大于10个字符
- 密码必须大于3个字符小于20个字符
- 账号必须唯一,同一个平台下的账号被删除后,可以创建相同的账号
在编辑用户
的页面上,和产品讨论了限制条件:
- 用户自己不能编辑自己(包括管理员角色)
- 只有管理员可以编辑用户(其他非管理员用户没有编辑用户的权限)
这些原型全都属于一个迭代里的需求,需要把已有的几套有关联性平台的用户体系都进行调整。
约定
在需求评审会议上,添加用户
页面的限制条件被忽略,通常都认为这是一个很简单的功能,能够达成共识,所以一句话带过。在一些细节上没有做到很好的约定。在后续迭代过程中,产品可能因为用户的反馈,将添加用户
页面的限制进行调整。有的限制条件没有及时在原型上进行标注,导致产品、测试、前端、后端的需求不一致问题。
在迭代开发前,后端和前端约定了3个接口,分别是:用户列表查询,用户详情查询,新增与编辑用户接口。
实操
因为这不是一个新项目,所以代码中已经有一些封装好的方法了,比如根据用户账号名获取用户信息getUserByUserName
方法,根据用户id获取用户详情getUserById
方法。底下所写的SQL很自然的会认为他是一个没有问题的SQL,毕竟项目运行这么久了,有BUG线上早就出问题了不是吗? 这种思想是非常危险的!!! 首先,我们拿什么保证自己的代码是没有问题的?工程中的单元测试没有覆盖到DAO层的方法,也没有进行系统集成测试。之前的稳定完全靠测试验证然后不断的改BUG来完成。有些边界值可能测试也不一定会覆盖得到,线上没有问题只能反映当下的业务逻辑没有触发到问题所在,不能等到线上出问题了再来排查。
其次,查询一个用户不仅是根据id查就完事了,还要根据当前的业务规则,比如查询没有被标记为删除并且所关联的平台账号没有没禁用或者删除的用户。
新增用户
根据页面原型,定义入参字段,添加入参限制。
坑1:用户启用状态status
字段在用户表中的类型为tinyint(1)
,映射到Java的PO类为Boolean类型。但是另一个表的status
字段类型却是tinyint(3)
,映射到PO类为Byte类型。虽然类型都是tinyint
但是因为长度的不同导致所映射的PO类型出现不同。两套平台的代码都是要按照原型来修改的,因为用户体系一样,表结构一样,所以我就把入参实体直接拷贝过来,但是却忽略了两套平台的工程所用的ORM框架是不一样的,导致ORM无法匹配到PO字段的类型,数据传到PO后status
字段值却无法更新到数据库中。
坑2:新增用户,原型的要求是 同一个平台下的账号被删除后,可以创建相同的账号
,这个功能在另外的一套相同用户体系中已经实现过了,于是使用已有的方法getUserByUserName
查询是否存在相同名称的账号,如果存在就返回提示不允许添加相同账号。把Controller和Serveice相关功能的代码直接拷贝过来,修改入参实体名,导包,修复红色错误提示等一些操作。直到代码没有语法错误后就觉得大功告成了,只需要运行验证一下即可。这种做法是非常危险的!!!
这个时候很可能临时有其他更高级别的任务被分配进来,然后中断当前的任务,去做其他事了。等回过头来就有一种已经验证过这个功能的幻觉。因为在其他套工程中确实是验证过了,但是在当前工程中还没有验证过! 刚好这套工程一直以来在DAO层一直都有一个getUserByUserName
方法,这个方法底下的SQL只根据账号名称查询用户信息,并没有根据用户当前所在平台的删除状态进行过滤,导致已经删除的用户名无法再被该平台新建。
编辑用户
编辑用户与前端约定的接口是和新增用户同一个接口,接口根据时否有传用户id作为新增和更新的依据。
坑3:传用户id进行更新的时候,是否需要把用户的所有信息都传过来?前端如果不传值过来,后端是否会更新对应字段?还是将对应字段更新成空值?
根据当前的原型设计,在用户详情页面中没有密码输入框,所以前端应该不传密码。但是我的接口逻辑没有跟着原型修改,无论是新增还是编辑,都会判断密码不能超过3个字符小于20个字符。前端不传密码的时候就会返回限制信息。
坑4:用户账号能否修改,将一个账号名改成另外一个账号名?
站在实际的业务角度出发,用户的登录账号是不应该被修改的,因为登录账号相当于一个用户的系统登录凭证。但具体能不能修改并不是绝对的,还是要看这个系统所处的位置和功能需求。在原型设计上,并没有标注出账号是否能被编辑,而我在实际开发中也没有注意到这一点,所以根据约定、前端如果传了用户Id,那么就会根据用户Id对用户的信息进行更新,包括登录账号。这时候又挖了另一个大坑。
首先假设需求上允许修改成另外一个用户名,从接口功能上,我没有对这种情况做限制,通过接口,用户可以将自己的登录账号更新成另外一个已经存在的用户名。原则上前端把用户账号的输入框限制成不允许编辑就不会存在这种情况。但是在前后端分离的项目中,后端接口必须考虑这种情况。现实场景中用户可能通过一些手段跳过前端页面限制直接请求接口。
其他的一些问题
关于 用户昵称不能大于5个中文字符,不能大于10个英文字符
这个需求有点奇怪,他是以数据库的视角来考虑长度的,在MySQL的varchar类型字段中,中文占用2个字节,英文占用1个字节,1个中文字符的占用大小等于2个英文字符的占用大小。其实这个存储空间和字符长度是没有关系的。
在程序设计角度考虑,这个功能限制其实是用户昵称不能超过10个字节
,只要遍历每个字符的ASCII码,大于255的都是非英文,这样就能统计出英文和中文的个数了。
这种限制在用户实际使用中会出现很奇葩的问题,出现了无限多可能。输入了3个中文后,只能再输入4个英文。输入了4个中文后,最多只能再输入2个英文,比如中国人abcd
,中华人民ai
,这些字符长度已经达到了10个字节,中英文混用的情况用户还要考虑一个中文等于多少个字母…
有些不合理的需求,或者变动比较频繁的需求,开发人员应该跟产品经理讨论具体的方案。否则出问题的时候就会导致BUG满天飞。这次修改涉及到多个端的工程,不是少这就是少那,把所做的工作变成了一个逐渐被否定的过程,真的感到很惭愧。希望后续能够统一用户体系和设计,把用户管理这块功能独立成一个服务,避免做重复的无用功,把单元测试和系统集成测试都完善才能保证质量。
怎样才能保证代码质量
想快速的解决问题,却不能稳定的持续
为什么我的代码没有质量保证
Q:现在线上运行的工程,他的业务规则是什么样的?允不允许这样或者那样的边界情况?会报什么错有哪些提示?
也许自己当前正在着手维护这套工程,可以马上回答出来,但是如果是半年前开发的并且中间跑去开发其他项目了,那么这个问题就不一定能马上回答上来了,跑去看代码心里也是没有底的,因为代码中不会太直观的体现出所有的边界情况,除非写了一大段的文字注释。如果这套代码是从其他开发者继承过来的,可能还会有注释和代码不匹配的情况。
在复杂多变的业务领域,我们无法用自己的基础知识去保证代码质量。业务庞大且错综复杂的时候,没有理清关系很容易陷入进去。之前的很多需求都是口口相传,很久之前做的业务逻辑基本都忘记了,看原型还要想好久才能想起来,而且还不能很确定。这些东西没有一个记录,即使有文档也要重新理解很久。
以后要如何做
放慢脚步,戒骄戒躁。用心写单元测试
欲速则不达,关注了数量,却忽略了质量。发现缺少某个接口,以最快速度写完提交代码发布测试服,虽然在很多情况下接口可能是没有问题的,但是如果非要去深究,每个接口可能都是有问题的!前后端分离的项目,后端真的对每个参数都做了严格的限制了吗?很多限制其实还是依赖前端自身的输入限制,至少我在补单元测试的时候发现很多限制都没有做。很多琐碎的任务不能成为不写单元测试的借口,如果静下心来写单元测试,是不是琐碎的需求就会少一些了呢?
某位领导曾经说过,在变更控制系统里的记录是很必要的,我们没事的时候不会随便去动业务代码,应该多去学习,提高技能。当需要我们动业务代码的时候无非是两种情况:有需求、有缺陷。
每一个需求和缺陷都尽量能够清晰的表达,并且指派给对应的人去完成。每一个需求和缺陷的规模点和完成时间应该把写单元测试的时间也考虑进去,因为单元测试是开发人员对自己代码质量的保证,如果自己都没有自检过,那这个任务不应该算完成。可能会造成一个功能在开发和测试之间反复流转。
单元测试是代码的防腐剂,代码开箱重新修改,对应的也需要重新更新防腐剂,否则代码将没有质量保证。
保证代码质量是一个不断提升和不断学习的过程,道路阻且长,努力加餐。