世上最简单的事是判断是非,依靠常识就能给出答案。世上最难的事也是判断是非,多少人把自己的傲慢与偏见带进了棺材。
昨天看到一篇义正言辞的IT博文,让我感慨良多。
博文标题叫对不起,因为之前的代码写的烂,所以我也只能继续烂。正话反说,洋溢着满满的正能量。
主要的故事情节摘录如下:
############################## 起始标记 ##############################
上周四,我团队里的某技术经理在一次代码评审会上跟一位开发同学发生了争吵,而事情的起因是某段看似合理,却存在明显性能问题的代码片段。
图1. 利用时间格式生成一个数据主键
图2. 在调用方法之前,先随机休眠
我先来做下简要说明,图1的代码是一位离职员工写的,而图2的代码是这位开发同学写的,把这两段代码拼凑起来,基本可以得出这样一个程序逻辑:
1、执行 getDataId() 会返回一串格式为 “当日日期+时间+毫秒+随机0-9” 的编号。
2、调用 getDataId() 之前,为了防止由于线程争抢或并发而导致的 “编号重复问题”,在调用之前,故意让当前执行线程随机休眠一段时间。
听到这,相信写过代码的同学都会产生一个质疑:“为什么要随机休眠?”
是的,这就是他们俩争论的原因。
先别急着吐槽,如果只站在 “执行通过” 的角度,你觉得这代码有问题吗?
粗糙的说,这代码不但能运行,而且作为一个企业内部系统,只要并发不达到一定规模的情况下,是绝不会出现异常的。
既然如此,那为什么还要争吵?下面我来还原一下他们俩的对话内容。
……
技术经理问开发:“调用之前为什么要增加随机休眠?这样做,你难道不考虑性能问题吗?”
开发说:“不是,我有考虑过。”
技术经理提高了嗓门:“考虑过干嘛还这么做?为什么不用同步(线程间互斥)来处理呢?”
开发听完反而来劲了,说:“我有提出过这个方案,但可能是涉及到的改动点比较多,大家都没理我,所以我也就没有坚持。”
技术经理反问:“什么叫没有坚持?你为什么不跟我来说?或者直接做一个新方法,甚至是把这个方法改了也可以呀。”
开发有些委屈,说:“不是啊,你给我的任务时间就一天呀,如果重构,除非搞个通宵,否则时间肯定来不及。再说了,公共方法又不是我负责的范畴,而且getDataId也是离职的人搞得。他们把代码写成那逼样,我有什么办法,又不是我弄得。”
开发耸了耸肩,继续说:“一个内部系统能有几个人用?怎么会有性能问题?没必要那么认真吧。接手那么烂的代码,我能把逻辑跑通就已经很不错了。”
技术经理明显被这一堆歪理激怒了,瞪大了眼睛说:“你这叫什么歪理?如果你觉得前任做的东西够烂,那你应该去想办法改善他们啊。“
“按你的逻辑,因为之前的代码写的烂,所以你也只能继续烂,是这意思吗?”
开发愣了一下,看了一眼技术经理,可能是顾及自己的KPI,从牙缝里挤出几个字:“那好吧,就按照你说的方式改吧...”
……
他们对话的时候,正巧我从会议室门口路过。
整个过程听的比较完整,但我并没有说话,更没有试图带着 “乌纱帽” 去对谁进行训斥,只是略微的缓和了一下两人之间的尴尬氛围,随后便各忙各的去了。
话虽如此,我慢慢地走回到自己的办公桌前,努力想让自己的内心平复下来,可是却无法平息内心的那股不爽。
我真不敢相信,这一番歪理居然是从一个年轻人嘴里说出来的。
############################## 结束标记 ##############################
图一对应的代码抄录如下:
/**
* 生成数据主键
* 时间(15): yyMMddHHmmssSSS
* 机器(): xxx
* 随机: 1
* @return
*/
public static long getDataId() {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyMMddHHmmssSSS");
String idStr = dateFormat.format(new Date())+""+ipAddress+""+getintRandom(0, 9);
long id = NumberUtils.toLong(idStr);
return id;
}
图二对应的代码抄录如下:
// 读取
FileReader fr = new FileReader(destFile);
BufferedReader br = new BufferedReader(fr);
String line;
while((line = br.readLine() != null)) {
if(!"".equals(line.trim())) {
detailDto = new CcaDetailDto();
Thread.sleep(1L);
datailDto.setId(GenerateUtil.getDataId());
detailDto.setLogId(logId);
detailDto.setLogContent(line);
detailDto.setCreateUser(userName);
detailDto.setCreateTime(currTime);
detailDto.setUpdateUser(userName);
detailDto.setUpdateTime(currTime);
detailDto.setStamp(0L);
detailDtoList.add(detailDto);
}
}
乍一看,技术经理尽职尽责,精益求精,开发小兵得过且过,当一天和尚撞一天钟。
但事实真是如此吗?判断是非的标准又是什么?
套用一句俗语,脱离情境去讨论是非就是耍流氓。
举个浅显的例子。
某员工上班时间天天打瞌睡,每天能打瞌睡十次左右。
看到这里,几乎所有人都会认为这位员工工作态度有问题。
但是如果我告诉你,这位员工每次打瞌睡的时间都只有一秒,还会有人这么认为吗?反而会觉得他是典型的称职模范好员工吧。
放在程序中也是一样的道理。
程序在运行时去睡觉(线程休眠),必然会延长程序执行时长(影响性能)。但是每次只延长1毫秒,也就是千分之一秒,真的会对系统造成什么负面影响吗?
程序设计出来是为人服务的,程序的好赖也应该以对人的影响作为衡量标准。
百度有上亿用户,日搜索量应该不下百亿次吧。
如果百度搜索的速度比原来慢了千分之一秒,你会感知到百度搜索的用户体验比原来差了吗?
相反,如果搜索速度慢了100毫秒,但同时添加了个酷炫的搜索动画,这样用户体验反而会更好吧?
相比于百度庞大的业务量,这个所谓内部系统的负载应该不到百度的九牛一毛,公司的发展真的会被这千分之一秒的延迟所羁绊吗?
煞费苦心搞出日期+时间+IP地址+随机数作为ID,按正常逻辑来思考,应该是用作业务单号吧?
做一单业务还纠结千分之一秒的延迟,这家单位的业务怕是遍布银河系吧?
退一亿万步讲,假如这家公司的业务真的遍布整个宇宙,这位技术经理的“线程互斥”方法真的可以避免重复ID吗?
“线程互斥”方案是规避掉了一段代码同一时刻被多次执行的问题,但是先后执行呢?先后执行就真的不会产生一样的ID吗?
代码规避重复ID,使用的是系统毫秒数,但是你能确保先后执行的两坨代码,不是执行在同一个毫秒内吗?
你或许会说,这是业务代码,逻辑复杂,况且还是在套壳的Java虚拟机里,执行哪有那么快,怎么着也得几十个毫秒吧?
姑且当你说的没毛病。就假设这段代码执行完成至少需要十个毫秒。
但是你有考虑过将来会发生什么吗?
举个不是不可能发生的例子。
假如某一天,公司的IT部和采购部想捞点油水,采购了十倍性能的计算机做服务器,导致原本10个毫秒才能执行完成的代码,现在只要1个毫秒就够。
换了十倍性能的服务器,原本稳定运行的业务却因此发生故障,想必不是公司老板想看到的结果吧?
你可能会说,你们公司不可能这么腐败,计算机更新换代也不可能那么快。
但是,你能保证Java程序获取到的系统时间就是正确的吗?就是没有误差的吗?就是持续增长的吗?
你知道Java里最常用的获取系统毫秒数的方法System.currentTimeMillis()
可能有几十个毫秒的误差吗?
就算Java没有误差,Java的时间也是从操作系统取的,你能保证操作系统没有误差?
操作系统时间是问CPU要的,你能确保CPU不会出故障?
CPU时间是从主板取的,你能确保主板不会出故障?
主板的时间要靠纽扣电池供电来保持,你能确保这颗纽扣电池电量永远用不完?
就算主板用不坏,你能保证公司不会突然更新换代换个主板?或者换个服务器?
就算以上情况都不发生,您能保证服务器运维管理员不会有事没事给系统校个时?
“线程互斥”这个看似标准的答案,放在这里其实漏洞百出。
就像防止瘟疫上身一样,只有一天二十四小时住在无菌室,才能真正万无一失。
从中级程序员晋升到高级程序员,“线程互斥”的技术应该是必备考点。
这位技术经理勤学苦练摸爬滚打上来,拿着锤子看什么都是钉子,想必才是发生这个故事的缘由所在。
但是他们讨论的这个问题真的就有那么难吗?真的有必要搞那么复杂吗?
对于这个业务场景来说,在数据入库的时候拦截掉重复ID,就是直接了当的解决方案。在数据库做个唯一约束,直接规避此问题。
但是很显然,这样做真等到极端情况发生,可能会导致程序奔溃。
解决方案是什么?不解决。
客户端奔溃就客户端重启,服务端奔溃就服务端重启,网络请求失败就重新请求。你要是能保证这套系统在这个地方奔溃的概率比所有其他地方加起来都大,想必比尔盖茨很乐意一百亿年薪邀请你去开发Windows 11
。
技术是为人服务的。代码的好赖要归结到代码对用户的切身影响上来。
代码的用户有两类人,接触此代码的程序员和使用最终产品的客户。
所以,代码的好赖,本质上是对这两个群体影响的好赖。
站在客户的角度,能按照合同百分百达成,使用上也不会碰到由于疏忽而没有出现在合同里的问题的产品,就是合格的产品。
如果客户对刷新网页延迟一毫秒没有感知,这段代码对于客户来说就是合格的代码。
但是站在程序员的角度,一段代码是否合格很难有评判标准。毕竟能服务好平庸的客户是本事,能服务好平庸的程序员,只能靠拉低自己,让自己够平庸。
所以,对于代码的好赖,不能吹毛求疵,抓住重点就行。
一段代码的好赖,重点在哪里?我觉得是这段代码的影响范围。
这行所谓的垃圾代码Thread.sleep(1L)
,只是调用系统函数,让代码小睡了一毫秒而已。就算脱离上下文,任何Java程序员都知道这行代码是什么意思。
这行代码单独来看,没有任何歧义。
但是放在这个上下文里,确实让人不明所以。虽然人畜无害,但没事睡个一毫秒干啥?
其他程序员或许会搞不明白这行代码的用途,但是不管这行代码是烂或是不烂,它的影响范围也仅限于这十来行代码块,不会将影响放射到其他代码。
与动辄影响成千上万行代码的垃圾代码来对比,这样的所谓“垃圾代码”简直不值一提。
这位技术经理要是够闲,应该去关注关注这块代码真正烂的,影响整个系统的垃圾代码。
先是"".equlas(line.trim())
。明明是判断一段文本是不是等于空白,为什么非要写成判断空白是不是等于这段文本?判断我的支付宝余额是不是零,为什么非要写成零是不是我的支付宝余额?脑子没事拐这么一个弯真的有意思吗?
再是detailDto.setCreateUser(userName)
。你的业务单真的创建了一个用户吗?为什么要弄个createUser
的属性?
createUser
这个字段真的是表示用户吗?表示的不过是用户名罢了,用User
后缀真的不会造成歧义?不会影响以后的扩展?以后真要表示这个用户的时候你要叫createUserObject
吗?
userName
有必要写成两个单词吗?username
已经是用户名的国际通用惯例了吧?就算这词不合理,用的太多了也录入牛津剑桥词典了吧?用username
、firstName
、lastName
、fullName
表示各自本意,国际通用不造成任何歧义,少绕绕脑子不好吗?
改成detailDto.setCreatorUsername
还会存在歧义吗?
detailDto.setUpdateUser
也是一样的道理。你的单子没有更新用户,就不要这么写。
再是detailDto.setCreateTime(currTime)
。你的单子创建了时间吗?上帝都只能创造光,创造不了时间,你这单子的大能超越上帝了吧?
再说currTime
,把currentTime
简写成了currTime
。既然在用Java
,就应该按Java
惯例来编码。Java
标准库的代码不到万不得已不会使用单词简写的吧?
真正追求极致代码的话,改成currentTimeMillis
符合Java
惯例,带上单位又避免了歧义的产生。
detailDto.setUpdateTime
也是一样的道理。你的单子没有更新宇宙时间的能力,就不该这么写。
最后说detailDto.setStamp(0L)
。current
缩写成curr
不会有多少歧义,timestamp
(时间)缩写成stamp
(邮票)就不好了吧?
0
后面跟个毫无用处的L
,不知道一个整数从4个字节塞到8个字节里,转换过程不可能溢出不可能失真吗?就像将一小碗饭放进一个大碗里,有任何难度任何风险吗?
这些虽然都是一些细节问题,但管中窥豹一叶知秋,这个系统想必处处充斥着这样的代码。每天每次跟这些乱糟糟的代码斗争完后,程序员能剩多长时间来写真正有用的代码?
长篇大论这么多掰回来的是非就是真正的是非吗?
保护环境是好事。但是假如从明天开始,十几亿中国人都不再乱扔垃圾,自己的垃圾自己处理。用不了多久,上百万清洁系统的人就得下岗,多少家庭会因此揭不开锅?想必闹出的人命都不在少数。
表面上在保护环境,实际上却断了人的生路。
放在IT界也一样。假如所有程序员都跟格雷厄姆一样,写代码直截了当,直击灵魂,直接就会导致全球上亿人的失业。
再放长远点来看,假如IT界由于效率太高发展太快,不出几年,全世界理论上可以被程序化的工作几乎全部都会被机器取代,地球上百分之九十九以上的人失业是在所难免。就算你有幸生活在一个非零福利国家,没有工作的日子过久了也会让人发狂。假如人类再碰到一个像希特勒那样太讲究实用主义的大独裁者,地球上史无前例的种族灭绝怕是也会被排上日程。
所以你表面上追求的是高质量的代码,本质上可能是在葬送人类的未来。
管子曰:“道在天地之间也,其大无外,其小无内。”站在不同的高度,看到的真理也不一样,真正真的真理应该也是没有尽头。在尊重真实的前提下,保持良善,保持宽容,永远保有敬畏之心,这样才能在追寻真理的道路上越走越开阔吧。