代码的写法有很多种:有的运行起来很快,有的只会占用少量内存,有的更容易测试,而有的代码则有很高的可读性。
若要编写思路清晰的代码,第一步就是要将可读性放在第一位。
这也意味着势必要降低其他因素的优先级。如果把所有因素都作为最高优先级,就意味着没有优先级。
想要写出好代码,首先要知道什么才是好代码,想要写出思路清晰的代码,也要了解什么才是思路清晰。多阅读一些质量上乘的代码可以让我们对好代码有个大概的认知。
了解什么才是优秀代码并不能杜绝我们继续写出糟糕的代码,但至少能让我们知道代码的哪里不对劲。
编写代码时,我们最初所想的思路未必清晰。在大多数情况下,只有在第一次完成代码后,我们才能找到更适合的思路。反复阅读已完成的代码才会带来更改的空间。
如果我们还搞不清代码结构,那么可以试着想象一下怎样向他人解释清楚或者把逻辑思路写下来,比如“如果删除账户,那么我们需要跳过 xxx。如果 xxx 的进程还没有结束,那么……”。然后把这套逻辑翻译成代码就很顺了。
写程序时,带入人类沟通方式而不是计算机中的抽象概念要更容易。
代码中的注释可以解释某段代码的用处,或者是程序结构为什么要这么写。
单单是阅读程序并不会告诉我们作者所想就是正确的逻辑。里面可能会有我们不了解的商业规则:美国境外的用户有时会把街道名写到地址栏第一行的最末尾。里面也可能有一些技术小技巧:以某种奇怪的方式构造查询,从而让 Postgres 正确地优化它。诸如此类的代码细节,都是只有了解逻辑背后的背景情况下才能彻底明白为什么要这么写的。
代码不会说话。如果我们决定跳过某些步骤,但又懒得留下注释解释为什么,过两天再回来看这段代码恐怕就真没人知道你当时在想什么了。
部分代码可能读两遍就能想明白个中缘由,但为了保险起见,还是不要给自己的大脑添加不必要的负担。
不要搞混函数中的抽象层次。
这段“欢迎”代码层次混乱:
def welcome(self): results = db.query( 'SELECT EXISTS 1 FROM emails WHERE kind = ? AND user = ?', 'welcome_email', self.user.id, ) if results[0]: return self.send_welcome_email()
这段则是相对整齐的:
def welcome(self): if not self.has_sent_welcome_email(): self.send_welcome_email()
函数中混乱的抽象层次会让读者思考代码用途和实现方式时被迫进行思维跳跃。当前抽象层次的代码告诉我们代码在做什么,而下一层次的代码则是关于代码要如何实现的。
在例子里的“welcome”函数中,我们首先在数据库中查询是否有过往邮件记录,如果没有则发送一封欢迎邮件。请注意,第二个版本中的“welcome”函数将查询部分放到了另一个函数中,“welcome”中仅仅关注“做什么”,这就是将函数中的抽象层次保持在了同一层,逻辑也更加清晰。不同函数分散在不同抽象层次,将较低层次的实现细节委托给较低抽象层次的函数。
有时,分解大体积函数到子函数会更便于阅读。
对于分步骤执行的函数,将函数中的每个步骤都分解成子函数效果会更好。而对于其他如决策类的函数,不同的决策会引向不同的函数:有的部分负责制定决策,有的则是负责执行决策。分解函数的方法有很多种维度,只有通过不断的练习才能一眼看穿哪种才是正确的。
小体积函数有以下几点好处:
其实,没有任何函数计算机也能运行得好好的,函数的存在只是为了服务于程序员,所以还请多多利用它们。
不要重复你自己(don't repeat yourself, DRY)的意思经常被过度解读。
如今,抽取魔法数常量,以及针对某类特定决策的逻辑副本,已经算是公认的标准答案。此类重复的代码的确不好。而 DRY 的过度解读是指面对区区两行的重复代码,便如临大敌恨不得除之而后快。完全避免任何的重复代码意味着我们最后将面对一堆毫无意义、令人迷惑的代码,其存在只为了防止程序中的两三行重复代码。再加上由于在逻辑上毫不相干的两段代码被迫捆绑在一起,代码也更加难以修改。
判断一段代码的重复是否可容忍很简单:修改 A 段代码,保留 B 段不变,如果程序报错,那么就把 A 和 B 整理到同一段代码;如果无事发生,那么就放着别管。DRY 并不代表我们需要手动压缩代码库,而是为了避免两段代码要依赖于手动的同步。请记住,重复代码和抽象创造并不是同一件事。
宁可要十个零参数的小函数,也不要一个带十个参数的函数。
诸位对类似的事一定不陌生:初始干净的函数,只在三个不同的地方被调用。而当我们想要在第四处调用时,我们需要做一点小的调整,添加一个参数。但这样第一个 caller 就多了一个新功能,也需要多添加两个可配置的参数。等到第五个用例,我们还要再为它添加独特的参数,以此类推。但反过来我们就又会发现第二个 caller 跑起来太慢了,所以只好再添加另一个参数来跳过部分繁琐的程序。
不知不觉中,我们那个干净整洁的、只负责一件事的函数现在有了五个配置参数,现在能做的事情甚至可以达到 2 的五次方种!
这种情况下,将这一整个复杂的函数拆分成子函数,每个函数只负责各自的事就会好上很多。
但这样以来,又不可避免会出现重复。当这些重复的部分需要保持同步时,我们可以利用 DRY 的思路,将相同的部分抽取到子函数中。这时,做决策和考虑步骤就会容易很多。
请记住,区区几行重复代码是没问题的!像是在不同 list 上跑 for 循环的代码,这类就是可以接受的重复。
这种方法的好处之一是当其中一个用例被删除时,你可以轻松删除掉对应的函数,而不是在复杂函数的逻辑里掘地三尺试图找到对应的选项。只关注某个特定函数的读者也会更容易理解它们的用处。
(注意,当你能负责所有的 caller 时,这种方法才是正确的。如果你的函数只是公共 API 的一部分,那么请不要考虑使用这种方法。因为你并不清楚所有的用例都是什么,也不知道未来会有什么样的用例)
竞速赛车跑得比普通轿车要快,这点毋庸置疑。但这也是赛车在牺牲了柔软座椅、低噪音,以及车载空调的条件下。如果我们的程序不需要做竞速赛车,那就不要过早地拆掉空调。逐渐熟悉程序的构造,先从编写易于人理解的代码开始,不要一上来就试图挑战计算机的运行速度。
同理,也不应过早开始泛化。没人会在不需要处理大量物品的时候就买入一辆自卸货车,在没有过多需求的时候,我们也不用提前编写多余功能的代码。
原文链接:
[http://jeremymikkola.com/posts/2021_02_02_how_to_write_readable_code.html](
领取专属 10元无门槛券
私享最新 技术干货