有一位知名的 Go 语言程序员和导师。他已经写软件 40 年了,认为自己开始懂得如何写好代码了。以下是他的经验分享:
我相信我们都支持“干净的代码”,但这是一种十全十美的东西,没有人可以合理地反对。
当然,问题在于我们中很少有人能就“干净的代码”达成共识,以及如何实现它。
从 ZX81 开始到现在,我的编程生涯还没有结束,我发现了一些经久不衰的原则。原则比规则更灵活,可能更广泛适用。
我认为好的程序应该是正确的(Correct)、可读的(Readable)、习语性的(Idiomatic)、简单的(Simple)和高效的(Performant)。
01、正确(Correct)
你的代码可能有很多精彩之处,但如果不正确,那么这些精彩之处就不重要了。
正确是首要品质。如果一个系统没有完成它应该完成的事情,那么在其他方面——无论它的速度有多快,是否有一个漂亮的用户界面——都不重要。但这说起来容易做起来难。
——Bertrand Meyer,《面向对象的软件构造》(Object-Oriented Software Construction)
实际上,代码正确意味着什么呢?直截了当的答案是“它做了程序员打算做的事情”,但即使这样也不完全正确!例如,我可能在错误的印象下编写了一个素数生成器,认为所有奇数都是素数。在这种情况下,我的代码可能生成我要求的奇数,但仍然不正确。
好的测试套件,就像十全十美的东西一样,几乎每个人都希望拥有。测试是任何好程序的必要但不充分部分。
一个良好编写的测试表达了程序员在给定一组情况下认为代码应该做什么,并且在运行它后,它是否真正做到了这一点。然而,测试本身也可能是不正确的。
这就是为什么我们需要对任何给定的测试都抱有怀疑的原因之一。它看起来像在说什么?它是否真的这么说的吗?这么说是正确的吗?如果测试验证程序的结果符合一些期望的结果,那么期望是否正确?如果测试声称覆盖某个特定代码段,它是否实际上以所有重要的方式测试了该代码,还是仅仅使其执行了一次?
没有测试是一个非常糟糕的情况,但是相比进行无效的测试,它稍微好一些。一个不准确的测试可能会让我们对代码产生虚假的信心。
因此,一旦编写了测试,请非常仔细地再次阅读它们,保持怀疑的眼光。程序员是不可救药的乐观主义者:尽管有很多证据表明我们编写的代码无法正常运行,但我们总是认为它会工作。相反,我们应该假设如果存在错误怎么办。谦虚并不是所有软件工程的美德,但对于好的测试编写者来说,这是他们所熟知的东西。
即使是经过彻底测试的代码,也不应该被认为是正确的,应该假定它是不正确的。
02、可读(Readable)
这听起来似乎不需要多说:谁在为不可读的代码辩护?至少不是我,但似乎周围有很多不可读的代码。当然,并不是说有人刻意地写不可读的代码;只是因为我们错误地把其他特质放在可读性之上。
性能就是其中一个特质,有些情况下性能确实很重要,但并不像你想象的那么多。
虽然可读性并不像正确性那么重要,但它比任何其他东西都更重要。正如丘吉尔所说的勇气一样,“可读性理所当然地被视为第一品质,因为它是保证所有其他品质的品质。”
怎样使代码具备可读性?我不认为“可读性”是你可以添加到代码中的东西,我认为它是当你删除所有使代码难以理解的东西后所剩下的。
再举个例子,一个选错了变量名的程序可能会让读者不知所措。同一个事物却用了不同的名称,或者不同的事物用了相同的名称,这会让人困惑。不必要的函数调用,仅仅是为了满足“方法应该少于 10 行”的规则,这会打断读者的思路。就像读一篇文章时遇到一个不熟悉的单词,你应该冒着失去思路的风险,停下来查一下它的意思,还是继续努力通过上下文来推断它的含义呢?
奇怪的是,让任何代码更易读的最好方法,是从阅读代码开始。这里我指的是一种特殊的阅读方式。我不是指匆忙地滚动代码页,浏览和略读以获取大致情况。
相反,我们需要用谨慎的、有序的、有意识的方式来阅读代码。我们需要从头开始,直到结尾。如果我们不清楚程序的起始,那么我们就要先解决这个问题。
当我们跟随程序的执行流程时,我们需要仔细阅读每一行代码,然后尝试回答两个问题:
请问这一行代码写的是什么?
请问这一行代码是什么意思?
例如,像下面这行代码:
requests++
这句代码表达的意思很明显:它将某个数值变量 requests 的值增加一。但不太容易弄清楚它的含义。在 requests 变量中计数的是什么?它为什么要增加?这有什么重要性?requests 的当前值是从哪里获得的?它的当前值是什么?何时何地会检查该值?是否有某个最大值?达到最大值时会发生什么?等等。
你可能对所有这些问题都有很好的答案,但这不是重点。重点是,读者能否通过查看代码来回答这些问题?如果不能,我们可以采取什么措施来提供答案或使它们更容易找到?通过像新手一样阅读我们自己的代码,我们可以以新的视角看待它。
以这种仔细、分析的方式阅读代码是真正学习编写代码的最佳方式之一。
03、习语(Idiomatic)
我认为“习语”这个词不太恰当:我更倾向于使用“传统”一词,但这并不符合 CRISP 缩写的含义。但是,当人们说“某某是习语”时,他们真正的意思是“这是传统的做法”。
我认为事物使用传统名称非常有价值:在 HTTP 处理程序中,请求指针总是被称为 r,而响应编写器被称为 w。如果有普遍的传统,那么遵循它是值得的。你还会有当地和个人的传统做法。在我的代码中,任意的 bytes.Buffer 总是被命名为 buf,在我的测试中比较的值总是被命名为 want 和 got,等等。
err 是一个很好的普遍惯例的例子:Go 程序员总是使用这个名称来引用任意错误值。虽然我们通常不会在同一个函数中重新使用变量名,但我们确实会在整个函数中重复使用 err 来表示可能出现的各种错误。通过创建变体名称如 err2、err3 等来避免这种情况是错误的。
这是因为它需要读者有更多的认知。如果你看到 err,你的思维可以无缝地理解。如果你看到其他名称,你的思维就必须停下来解决这个谜题。我称这些微小的障碍为认知微侵犯。虽然每个微侵犯的个体效应是可以忽略不计的,但它们很快就会累积,如果它们足够多,就会造成麻烦。
当你编写每一行代码时,你应该考虑“需要花费多少时间才能理解这一点?我能不能在某些方面减少这个量?”伟大软件工程的秘诀就是把许多小事做好。选择正确的名称,按照逻辑组织代码,每行代码只表达一个想法,日积月累你的代码就会易于阅读,且易于使用。
如何学习习语?通过阅读其他人的程序,就像我们阅读自己的程序一样仔细、有意地阅读。如果你从来没有读过小说,你就不可能写出一部好小说,同样的道理也适用于编程。(最好也阅读一两本精心挑选的关于编程的书籍,尽管这不那么重要。)
尽可能广泛地阅读代码是非常有用的,因为你会发现所有的代码质量都是参差不齐。阅读糟糕的程序和优秀的程序同样有用,即使是在优秀的程序中也会存在错误,当你发现错误时,你也会学到东西。
04、简单(Simple)
每个人都认为他们知道简单,但奇怪的是,实际上没有两个人能在“简单”的含义达成一致意见。
正如 Rich Hickey 所指出的那样,简单并不等同于容易。“容易”是熟悉的,不费吹灰之力的,是我们不加思考就会采用的事物。这通常导致“复杂”,所以从“复杂”到“简单”需要付出大量的努力和思考。
简单性的一个方面是直接性:它做到了它说的事情。它没有奇怪和意外的副作用,也没有将几个不相关的事物混为一谈。直接性与简洁性成反比:与其写一个短而复杂的函数,不如写三个简单而相似的函数。
这是人们发现难以编写简单代码的一个原因:我们都害怕重复自己。“不要重复自己”的原则已经根深蒂固,我们甚至将其用作动词:“我们需要优化这个函数”(正如 Calvin 提醒我们的那样,将名词动词化会令语言变得奇怪)。
但重复本身并没有什么问题。我再说一遍,重复本身并没有什么问题:我们经常做的任务可能是重要的任务。如果我们发现自己创建新的抽象只是为了避免重复,那么我们就错了。我们使程序变得更加复杂,而不是更加简单。
这就带我们进入简单的另一个方面:节俭。事半功倍。一个只做一件事情的程序包比一个做十件事情的程序包更简单。函数越少,调用栈越浅,程序就越简单。这可能会导致一些长函数,但这没关系。函数应该恰到好处,长度适宜。为了缩短函数长度而增加不必要的复杂性,这只是盲目奉行规则违背常识的典型例子。
Alan Perlis 曾经观察到,简单并不是复杂的先决条件,而是其随后产生的结果。换句话说,不要试图编写简单的程序:先编写程序,然后将其变得简单。阅读代码,问它在说什么,然后问自己是否可以找到更简单的方式来编写相同的东西。
你可以在自然性中找到简单性。任何一种语言都有其自己的道,自己的自然形式和结构,与它们一起工作通常会得到比与它们相对抗更好的结果。例如,你可以尝试使用 Go 作为 Java、Python 或 Clojure,这些语言都没有问题,但是用 Go 编写 Go 程序更简单,结果也更好。
05、性能(Performant)
你可能会觉得我把这个放在最后很奇怪。难道几乎所有有关编程的听闻或阅读都与性能有关吗?是的,的确如此。但我并不认为这是因为性能很重要,而是因为它很容易谈论。什么可以被衡量就会被最大化,而性能很容易被衡量:你可以用秒表来测量。相比之下,像简单性甚至正确性这样的东西就难以量化了。
但是,如果代码不正确,谁在乎它运行得有多快?换句话说,如果不正确,我们可以任意地使一个给定的函数更有效。同样地,如果它不够简单,我们将浪费更多的时间来理解它,而我们所能节省的 CPU 时间远远不够弥补这个成本。而程序员每小时的运行成本比任何 CPU 都高。优化限制资源难道不是有意义的吗?
就像我之前说过的,对于绝大多数程序而言,性能并不重要。当它重要时,最好的解决方案通常不是使你的代码更难读。这里适用于“慢就是顺,顺就是快”。如果为了节省几微秒的时间而使你的程序陷入无望的复杂性,那很好,但那将是任何人对它进行的最后一次优化。试图加速复杂代码通常是适得其反的,因为如果你无法理解它,你就无法优化它。另一方面,当你必须加速一个简单的程序时,它很容易加速。
尽管我们不必让性能左右我们的选择,但我们应该意识到这些选择所带来的性能影响。如果我们不需要快速完成这项任务,我们也不会交给计算机。另一种思考方式是,一个高效的程序可以在给定时间内完成更多的工作,即使实际所用的时间并不重要。
公平地说,即使是效率低下的程序也会运行得相当快:正如 Richard Feynman 所观察到的那样,计算机内部很蠢,但它的速度很快。这并不意味着我们可以浪费时间。
当你编程时,“机械同理心”的概念非常有帮助。这意味着你对机器的基本工作原理有一些了解,你会小心谨慎地避免滥用它或妨碍它。例如,如果你不知道什么是内存,你怎么能写出高效的程序呢?
我经常看到一些代码毫不在意地将整个数据文件读入内存,然后只是每次处理几个字节。我是在一台只有 1K 内存或大约一千个双字节的机器上学会编程的,它甚至无法容纳这篇文章。
过了几年以后,我用一台大约有 1600 万个双字节的内存的机器来写这篇文章,而且这些字节的大小是原来的八倍,这意味着我们现在可以放松下来,使用尽可能多的内存了吗?没有,因为任务也变得更加庞大了。
一方面,你在系统中传输的数据越多,花费的时间就越长。另一方面,无论你的 Kubernetes 集群有多大,它仍然由固定、有限内存的物理机器组成,而且没有一个容器可以使用多于单个节点的总 RAM。所以,好好管理你的字节,千兆字节会自动照顾好自己。
关于Python学习指南
学好 Python 不论是就业还是做副业赚钱都不错,但要学会 Python 还是要有一个学习规划。最后给大家分享一份全套的 Python 学习资料,给那些想学习 Python 的小伙伴们一点帮助!
包括:Python激活码+安装包、Python web开发,Python爬虫,Python数据分析,人工智能、自动化办公等学习教程。带你从零基础系统性的学好Python!
👉Python所有方向的学习路线👈
Python所有方向路线就是把Python常用的技术点做整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。(全套教程文末领取)
👉Python学习视频600合集👈
观看零基础学习视频,看视频学习是最快捷也是最有效果的方式,跟着视频中老师的思路,从基础到深入,还是很容易入门的。
温馨提示:篇幅有限,已打包文件夹,获取方式在:文末
👉Python70个实战练手案例&源码👈
光学理论是没用的,要学会跟着一起敲,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。
👉Python大厂面试资料👈
我们学习Python必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有阿里大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。
👉Python副业兼职路线&方法👈
学好 Python 不论是就业还是做副业赚钱都不错,但要学会兼职接单还是要有一个学习规划。
👉 这份完整版的Python全套学习资料已经上传,朋友们如果需要可以扫描下方CSDN官方认证二维码或者点击链接免费领取【保证100%免费
】
