代码改变世界

编写明显没有错误的代码

2015-04-08 10:55 by 横刀天笑, ... 阅读, ... 评论, 收藏, 编辑

昨天公司的技术微信公众号贴了一篇关于怎么检索一天所有订单的SQL的写法问题。类似: SELECT order_no FROM orders WHERE create_time >= '2015-04-07 00:00:00' AND create_time <= '2015-04-07 23:59:59',文中提到这种写法是有问题的,可能有部分订单落在最后一秒(create_time是Timestamp类型),这样在进行对账等环节可能带来很隐藏的bug,很难被发现。文中还给出了MySQL中的方案推荐这样写 SELECT order_no FROM orders WHERE create_time >= '2015-04-07 00:00:00' AND create_time < '2015-04-08 00:00:00'。

就这么个问题在吃饭的时候和几个同事进行了热(面)情(红)洋(耳)溢(赤)的讨论。有同事说他会在传给SQL的参数的截至时间拼接上.999。也即把毫秒数给拼接到截至时间里,这样就能『正确地』查询当天所有订单了。看起来是正确的,因为就目前主流的MySQL版本(5.5, 5.6。5.5版本Timestamp精度支持到秒,5.6版本支持到毫秒)是能把一天的所有时间都包括。当时我们表示拼接上.999难以理解,这就相当于我们常说的magic number一样,会给维护代码带来障碍。大家觉得使用create_time >= '2015-04-07 00:00:00' AND create_time < '2015-04-08 00:00:00'更无歧义一些。不过这位同事给了另外一个理由:因为他在使用SQL定义时间范围的时候更习惯使用btween ... and ... 。好吧,如果这也算是一个理由的话。

不过这让我突然感到有点羞愧:我居然不知道between ... and ...表示的是开开,开闭,闭开还是闭闭区间。在询问了几个同事,都吞吞吐吐表示是闭闭区间,甚至有位同事还去查了下MySQL官网之后,我觉得SQL的btween ... and ...设计得真扯淡。使用between ... and ...比添加.999更恶劣,因为这让我这个不知道这个区间的人感到羞愧,最起码我还知道.999是毫秒。人在被人弄得羞愧后第一个反应就是理直气壮的进行反驳,以隐藏这种羞愧感。在思考了几秒钟后我反驳道使用between ... and ...不好,不推荐使用。如果你想来描述一个区间范围的话用大于等于,小于等于这样的方式会让代码更让人容易理解(比如,create_time >= '2015-04-07 00:00:00.000' AND create_time <= '2015-04-08 00:00:00.999',虽然我不想承认加.999是对的),代码更易读,因为就算不知道between ... and ...实际区间含义的人都知道这是什么意思(说完这句话,我的羞愧感大大降低了,甚至有点骄傲的感觉)。

但是,在我骄傲的时候,这位同事给我当头一棒:你不记得between ... and ...是你的事,但是我记得啊,我第一次看到between ... and ...的时候我就决定用她了,因为范围的意思就是between ... and ... 。言下之意就是between and就是范围,范围就是between and是多么【自然】的一件事情。其实我挺喜欢自然这个词的。在恼羞成怒的状态下,我抛出了杀手锏:你知道Junit的assert方法第一个参数是actual还是第二个参数是actual么?(画外音:因为Junit的这些只有两个参数的assert方法,两个参数类型是一样的,我每次使用的时候都搞不清楚第一个是actual还是expected,如果用错最后的错误提示也是反的,幸好Junit现在提供了assertThat的方式)。但是他居然记得,他果断地说第一个是expected,第二个参数是actual。看来记忆力不好是硬伤,世上总有些人比你记忆力好,他们记得所有东西,所以千万不要跟他们拼记忆。

那么说了这么多我想表达什么呢?我可不是为了吐槽我那可怜的记忆力的。其实上面两个例子只是想说,我们在编写代码的时候,应该时刻记着如何编写明显没有错误的代码。在性能没有量级的不同之前,我们更应该编写明显没有错误的代码(比如刚才那个查找一天订单的问题我觉得SELECT order_no FROM orders WHERE create_time like '2015-04-07%'是个更直观的写法,但是这种写法可能性能不好,所以只能作罢)。明显没有错误的代码就是即使别人来维护这个代码的时候缺少一些上下文信息(比如当前数据库使用的是什么精度,或者某方面的知识)都能很容易判断这个代码表达的是什么意思,因为当前他所看到的代码就是所有的上下文,没有其他隐藏的。我们在编写代码的时候一定要记着我们不是一个人在战斗,你的代码除了机器执行外会有更多的人来阅读和修改。在我看来SQL的between ... and ...和Junit的assert序列方法都是反面教材,他要靠人们对某些知识的记忆才能理解,但人的记忆又往往很不靠谱,就这样bug就出现了。即使在有些地方我们需要进行性能优化,可能把一些代码给弄得比较难以理解,那么我们也应该将这样的代码范围控制好,局限在某个部分。比如我们将这样的代码封装到一个类里,而不是分散到各个地方,并且我们用类名,用方法名,再不济用一些注释向后来者述说着这里曾经的故事。

后记

大部分开发人员(包括我自己),可能对性能优化更感兴趣。比如常见讨论方式是:这个地方我去掉了一个锁,那个地方我减少了多少内存,性能提高了多少多少。而很少见到这样的讨论:这个地方我修改了一个方法名那个地方我修改了一个变量名,现在我的代码更易懂,更【形象】起来了。其实在一个工程化的环境中,性能是不是重要呢?当然重要,但不是最重要的。而代码质量才是提高整体效率,降低故障率更有效的途径。而且更加讽刺的是,往往我们的性能到了需要优化的时候了,但是因为代码实在太糟糕而不知道如何优化,因为看不懂,不敢改。各个地方各种隐藏的上下文,各种歧义代码让性能优化难于上青天。你觉得修改这个地方好像没有问题,但是等你修改之后一个故障就开始了。

还有一些开发人员可能比较喜欢遵循自己固有的一些习惯。诚然,每个人都有一些习惯,可能是很早之前在某个角落看到一段什么话,然后就记下来了,后来一直遵循着这种习惯,但是又说不出真正什么理由(嗯,就是喜欢)。但是,我们在遵循这个习惯的时候,也要想着这是我的习惯,不是别人的。这个代码有没有更好的写法呢?所有人一看就知道是这个意思的代码或许比习惯更重要。

所以对我来讲,我虽然也很热衷性能优化,喜欢研究一些所谓的高并发高性能,但是我更想把我的代码写得【漂亮】,让别人维护起来的时候更容易看懂。编写明显没有错误的代码不是一件很LOW的事情,虽然逼格不高。