查看原文
其他

如何写出别人能读懂的代码

CSDN 2023-07-27

【编者按】这篇文章介绍了写出可读性高的代码的重要性和方法,包括命名规范、注释风格、代码结构和格式等。文章还给出了一些实际的例子和建议,如编写“小”函数、慎重取名、避免变量重新赋值、避免魔法值、使用尽量少的参数、写注释的艺术、避免重复等,帮助读者提高自己的编码水平和协作能力。

原文链接:https://stackoverflow.blog/2023/02/13/coding-102-writing-code-other-people-can-read/

未经允许,禁止转载!


作者 | Max Pekarsky       译者 | 明明如月
责编 | 夏萌
出品 | CSDN(ID:CSDNnews)


我们都已经身处编程时代


在20世纪80年代,个人电脑开始广泛应用。自从 1977 年 Apple II 的问世以及四年后 IBM PC 的诞生,原本只属于科学家和军事人员的工具开始在会计事务所、大学和医院中普及。你不再需要是工程师才能操作计算机。

过去十年,编程也经历了与此类似的演变。在日常的办公室、教室和实验室中,现在许多编写和维护代码的人并未自我认定为程序员。

幸运的是,如今编写高效代码比以往任何时候都更为简单。有大量面向初学者的编程教程,而现代高级语言的可读性也比过去有所提高。因此,如果工作中有人交给你一个Python脚本用来处理一些数据,你很可能能在 Stack OverflowCodecademy 上找到解决方案。

然而不幸的是,你编写的新手代码总是需要有人来看(这个人可能就是半年后的你自己)。对初学者来说,得到一个可行的解决方案就已经是达成目标了。但对开发者来说,这只是起点。特别是,我们现在编写和协作代码的频率比以往任何时候都高,因此写出更优质的代码显得尤为重要。

在编程的基础阶段,除了要让代码“运行起来”,还有许多其他东西需要学习。对专业开发者来说,编写可维护的代码是他们工作的核心。然而这些实践往往并未包含在 “编程101” 级别的教程中。初级教程通常只关注语法。

因此,我们假定你已经掌握了如何实现一个可行的解决方案——现在是时候升级你的编程技能到 102 级别了:为人,为他人编写代码。

前置知识

我们的示例将尽可能保持简洁,使用 伪代码 来编写。这篇文章主要针对那些花时间写代码,但并非全职专业编程人员的人。如果你对变量、函数和 if/else 语句有所了解,那么这将对你非常有帮助。


为什么编写优质代码很重要


在编程界,有句话:维护代码的成本占总成本的 75%。换言之,你所编写的代码,其他人(包括未来的你自己)可能需要花费两到三倍的时间来阅读、更新和修复它。这并非意味着你的代码写得糟糕或充斥着 bug,而是因为需求会不断变化,代码的其他部分也会随之调整,有时候可能会出现 bug。

我记得我在第一份工作(非编程)中,我编写了一个脚本,能够在多个带有用户账户的浏览器标签页之间进行切换,并粘贴折扣码。以前这项任务需要每个月手动为数百名用户操作一遍。

我花了几个小时匆忙编写出这个脚本,成功地在几分钟内粘贴了数百个折扣码,赢得了经理的称赞。然而,仅仅过了一个星期,脚本出现了问题,错误地将折扣码应用到了 300 个账户上。我花了两个小时编写这个脚本,但却需要八个小时来修复由此产生的问题和混乱的代码。这就是所谓的维护成本。

优秀的代码设计可以“降低未来变更的成本”。比如,如果我的公司更改了折扣码页面的布局,我需要多长时间来更新我的脚本以适应新的布局?如果有另一位开发者愿意帮忙,他们需要多久才能理解我的代码?这都取决于代码设计的质量。

为了使代码易于维护、更新和修复,我们应该力求使其模块化、易读和易于推理。编写"干净"的代码能帮助你更好地思考问题。如果你的团队成员都能遵循并执行这些优良的编程实践,那么你们将能减少在解决困难上的时间消耗,从而有更多的时间投入到高效的协作中去。


编程进阶篇


现在,我们已经对编写易维护、整洁的代码的重要性有了深入理解,那么接下来,我们来探讨如何编写这样的代码。

1. 编写“小”函数

我们应尽可能地编写只负责一项任务的简单函数。比如,你现在需要编写一个函数,这个函数的任务是接收一个零件价格列表,然后返回这些价格的平均值的文字描述。例如,你输入 [13, 28, 17, 143, 184, 72.3],函数应返回 "6 个零件的平均价格是 $72.22。"

初版的代码可能是一个名为 get_price_average(data) 的函数,它会:

  • 接收一段数据

  • 计算数字列表的长度

  • 计算价格的总和

  • 将总和除以零件的数量以得到平均值

  • 对平均值进行四舍五入

  • 将所有统计信息整合成一个文本字符串并打印出来

其中一些操作可能已经在有些编程语言中内置好了。将这些步骤整合到一个函数中,就构成了一个还不错的初版。

这段代码的优点是,这段代码具有内聚性:所有与统计文本相关的操作都在同一个地方。所以,如果你需要更改这段文本的任何部分,你都知道你哪里进行修改。

但问题在于,如果你确实还需要进行修改(比如添加一个统计项,澄清应平均哪些数字,或者更改平均值的小数点位数),你就需要阅读并理解整个函数,这会使函数越来越庞大。

解决这个问题的方法是,我们的 get_price_average()  函数负责太多任务了,需要将其拆分成几个只负责一项任务的小函数。

一个好的指导原则是:

一个函数应该简洁精练,只负责一项任务,理想状态下,不应影响函数外部的任何元素

我们通过一个例子来理解。以下是我们可能对函数进行拆分的方式。

  • get_widget_price_stats(data):我们正在对 get_price_average() 进行重命名。现在它接收一些数据,提取相关的统计数据,并返回包含这些统计数据的文本。稍后我们将讨论命名问题。

  • get_max(list_of_numbers) :获取数字列表中的最大值。

  • get_min(list_of_numbers) :获取数字列表中的最小值。

  • get_sum(list_of_numbers) :计算数字列表的总和。

  • get_average(list_of_numbers, decimal_points) :接收一个数字列表,返回精确到 decimal_points  的平均值。

你是否发现其中一些函数的描述非常清晰?例如,get_max(list_of_numbers)  就是获取列表中的最大数字。这就是我们设计的初衷。当你编写只有一个明确责任的小函数时,你可以给它们起一个清楚地反映其功能的名字,这样,你的读者就不需要阅读函数内的所有代码就可以理解它的作用。

这种设计如何应对需求变化呢?假设你现在需要返回的不再是平均值,而是标准差。在初始版本中,我们需要找到计算平均值的代码片段并用计算标准差的代码替换。

而在小函数版本中,我们只需创建新函数 get_standard_deviation(list) ,然后在 get_widget_price_stats() 中将调用 get_average() 替换为调用 get_standard_deviation()即可。

总的来说,我们拆分出的函数应该是小而完整的,且只负责一项任务。这样的设计可以大大简化你的代码的阅读和修改过程。养成这个习惯,就能显著地提升你的代码质量。

2. 慎重取名

在计算机科学界,我们常说这样一句戏言:“计算机科学的两大难题:缓存失效和命名。”在本篇文章中,我们将不讨论缓存失效,而专注于探讨命名问题。

我们有一个重要的编程理念,那就是好的代码应该能够自我解释。这意味着高质量的代码能明确且准确地阐述其功能和目的。命名在此过程中起到了举足轻重的作用:良好的命名应当具有精确性、易于理解,并能在无需阅读全部代码的情况下为代码提供清晰的上下文。

以下是一些值得参考的命名实践:

  • 描述意图:尽量精确地描述你正在命名的变量或函数。比如,我们在前文中看到的函数名 get_price_average,实际上我们返回的是小工具的平均价格,因此我们可以将函数名改得更精确些:get_widget_price_average()。随后,我们想让这个函数变为返回统计数据的函数,因此我们对其进行了重命名:get_widget_price_stats()。接着,我们从这个函数中提取出了一个通用的求平均数的函数,该函数接受任意数字列表并返回其平均值。

  • 避免通用命名,比如 dateslicepartnumbers 等。而是根据上下文的需要,可以将 date 改为 created_at,将 slice 改为 outdated_prices,将 part 改为 part_to_be_fixed,将 numbers 改为 current_row

  • 保持一致性:无论你选择哪种命名约定,坚持下去非常重要。如果你在代码库中使用 get_作为获取返回值的函数的前缀,那么每个函数都应遵循 get_[stat] 的格式,如 get_averageget_max 等。然而,如果你将一个返回标准差的函数命名为 standard_deviation,那么其他开发者可能会感到困惑,因为他们可能不清楚这个函数与其他 get_[stat] 函数之间的区别。

  • 提供适量的信息。一个命名为first_name的变量就能传达足够的信息。而替代选项 name 过于模糊(是名字的首部、尾部、中间部分,还是全名?),nfn 甚至更难理解。这些都是信息提供不足,使得变量的作用并不明确的例子。user_first_name 并未提供额外的信息,因为 user 过于通用,且替代选项不清晰(该应用程序是否存在机器人也拥有名字的情况?)。同样地,first_name_for_user_recordget_price_average_by_dividing_prices_by_quantity 这样的命名则提供了过多的冗余信息。

决定信息量的恰当度是一个随时间推移你将逐渐掌握的技能。这需要你后退一步,深思熟虑变量所承载的职责。长期来看,这将提升你代码的品质,并帮助你成长为更优秀的开发者。

在开始给变量命名(例如fn)之前,你应了解一些应避免的命名方式:

  1. 避免包含类型:你应该注重变量的作用,而非其实现方式。例如,常见的不良命名有arraynumber/numlistwordstring。由于代码的语法通常都已经明确了数据的类型,因此你无需在变量名中额外增加类型信息,例如,应避免命名为get_averagefunctionarray_of_prices。总体上说,你的命名不应该涉及变量的类型,这被称为"匈牙利命名法"。如果你打算使用admission_numbers,那么你应该寻找如daily_admissions这样更好的名字。

  2. 避免使用缩写det可能代表detaildetrimentaldeterministicDetroit,或者其它含义?我们之前提到的fn就是这样的例子。

  3. 避免使用单个字母的命名这种做法更为严重,因为这样的缩写完全没有可理解性。相反,你应该在命名时清晰地描述变量的用途。

然而,有些情况下是例外:

  1. 在循环中,我们通常将索引称为i

  2. 在科学和数学中的命名惯例,只要你确信其它开发者能够理解。

  3. 领域特定的命名惯例(例如,在JavaScript中,网页事件通常被简写为e,而非event)。

不过,这些都是例外情况,99% 的变量名应该具备描述性,使用多个字母。

3. 避免变量重赋值

以下的函数接受一些input,用seasonal_multiplier进行转换,再加上一个神秘的 14,最后返回result

function get_result(list, seasonal_multiplier) {int result = list;result = list.sum / list.length;result = result * seasonal_multiplier;result = result + 14;

return result;}

除了变量名含混不清之外,这个函数中的result曾经代表了四种不同的概念。起初,它是输入的数据,然后它成为参与者的输入的总和,接着又变为参与者的输入经过季节系数调整的结果,最后又加上了一个神秘的 14。这样的重赋值使得其功能过于复杂。

大多数的语言允许你给新的变量赋予新的值。然而,这并不是好的做法。一个变量应该代表一种含义,并且在其生命周期内保持不变。当你看到一个变量名时,你应该对其功能有清晰的认识。

以下有两种方式可以优化这段代码:

1. 明确每个转变的含义。一种更好的方式是明确每个转换的含义,每一步都声明一个新的变量。这可能起初看起来有些过于多余,但实际上可以使得函数更容易理解。

function get_result(list, seasonal_multiplier) {output_per_participant = list.sum / list.length;seasonal_output_per_participant = output_per_participant * seasonal_multiplier;adjusted_seasonal_output_per_participant = seasonal_output_per_participant + 14;

return adjusted_seasonal_output_per_participant;}

作为最后一步,我可能会将函数重命名为get_adjusted_seasonal_output_per_participant()

2. 将所有的计算步骤合并到一行。你可以通过将所有的计算步骤合并到一行,彻底避免重赋值的问题。尽管读者需要跟踪一大堆的数学运算,但至少变量的含义清晰,而且在整个函数中只有一个含义。

function list, seasonal_multiplier(input) {adjusted_seasonal_output_per_participant = list.sum / list.length * seasonal_multiplier + 14;return adjusted_seasonal_output_per_participant;}

你可能会好奇,这个“14”是什么含义呢?其实,这就是我们常说的"魔法值"。

4. 避免魔法值的使用

在编程中,我们称没有上下文含义的数字为“魔法值”,它们会大幅降低代码的可读性。让我们来看一段如下的代码:

int yearly_total = monthly_average * 12 + 67.3;

这段代码中的 monthly_average * 12 可以推断出是计算全年的月份总和,这个推断来自于变量名 yearly_total 的直接含义。然而,理想的代码应当明确到无需任何推断。那么,这里的 67.3 又代表什么呢?虽然写代码的人可能十分清楚,但对于其他人来说,这可能会引发困扰。

更为明智的做法是,将这些数字赋予具有清晰含义的变量名,然后在计算过程中使用这些变量。

int total_at_beginning_of_year = 67.3;int months_per_year = 12;int yearly_total = monthly_average * months_per_year + total_at_beginning_of_year;

5. 使用尽量少的参数

函数的作用是接收输入,我们把这些输入称为“参数”或者“实参”。在大多数编程语言中,传入参数的顺序是非常重要的。例如,你有一个函数,它需要输入水果类型、国家和年份,然后返回在该年份该国家的该类水果的平均价格。

function getAverageFruitPrice(fruit, country, year) { // get and return the price}

目前来看,这个函数的设计还是比较合理的。你可以通过调用 get_average_fruit_price("peach", "Albania", 2007)  获取价格。但是,如果我们想进一步限制查询范围,只查询卖给果汁公司的水果的价格呢?

我们可以通过增加一个布尔类型参数来满足这种需求,即 function get_average_fruit_price(fruit, country, year, sold_to_industry)。但你需要注意,参数的传递顺序必须正确,否则可能会引起混淆。每次调用此函数时,我们都需要检查函数的定义,以确保参数的顺序正确。如果你把参数的顺序搞错,例如写成 get_average_fruit_price("peach", 2007, "Albania", false),那么 year 就错误地变成了 "Albania"。

一个良好的编程原则是,尽量保持函数的参数数量在两到三个之间。这样,使用你代码的开发者就无需频繁检查参数顺序,减少出错的可能性。

对于参数过多的函数,我们有两种改进的方式:

一、拆分成更小的函数。参数过多可能是因为函数的功能过于庞大。

二、使用选项对象。如果参数可以以对象(在某些语言中被称为哈希或字典)的形式传入,那么就不用担心参数的顺序问题。这种对象通常被称为选项,或者“选项对象”。但这种方法也有一定的缺点,就是对于预期的参数定义不够明确。下面就是这种方法在实际中的使用方式:

function get_average_fruit_price(options) { // use options.fruit, options.country, options.year, options.sold_to_industry}
6. 写注释的艺术

对于代码注释,尽管存在许多详细的规则,我们这里重点关注一些显而易见的规范。

注释的作用是为了解释那些本身难以理解的代码部分。然而,如果你能优化这些代码,你就应该直接优化它们!比如以下这个例子,原本因为代码含义不明确而被不良注释掩盖:

int f = 75; // f is the temperature in Fahrenheit

这里,让代码直接解释自己的含义会更好,将 f 重命名为 temp_in_fahrenheit

有时候,你可能由于时间限制,无法即时改进那些难以理解的代码,甚至可能会写出一些自己并不满意的代码。在这种情况下,你需要考虑段代码是否会被其他人使用或者修改。毕竟,你肯定不希望传递那些难以维护的代码。

如果你认为代码可能不会被他人修改,但某段代码特别复杂,你可以留下一段注释来解释它的作用。理想情况下,你的函数和变量名应该已经做到这一点。但是,如果函数包含难以理解的逻辑,而函数名无法提供足够的帮助,这时候就需要你留下注释了。

7. 避免重复

DRY 原则,全称"Don't Repeat Yourself",即“不要重复自己”。如果你发现在不同的代码段中有重复的部分,可以考虑是否可以将这些部分提炼为一个变量或者一个函数。

string company_description = “Fruit Trucking & Shipping International ships fruit on our modern fleet of trucks and ships, anywhere in the world!”string company_slogan = “Fruit Trucking & Shipping International: where the best fruits get on the best ships”
string company_name = “Fruit Trucking & Shipping International”string company_description = company_name + “ ships fruit on our modern fleet of trucks and ships, anywhere in the world!”string company_slogan = company_name + “: where the best fruit gets on the best ships”
比如,如果公司更改了名称(例如,决定只运送苹果),我们只需修改一处。而不遵循 DRY 原则,则需要寻找并修改所有重复的代码。

再来一个例子:假设所有的温度输入都是华氏度,而我们需要的输出是摄氏度。在处理每一个温度的时候,你都需要进行 F - 32 * 5/9 的转换。如果你发现现在的系统开始使用开尔文温度,那么你就必须在所有需要温度的地方更新为 F - 32 * 5/9 + 273.15

一个更优的解决方案是将这个转换封装成一个函数:

function fahrenheit_to_celsius(temp_in_farenheit) { return temp_in_farenheit - 32 * 5/9;}

现在如果需要转换为开尔文温度,你只需要在一处进行计算更新。在这个例子中,你可能只需要创建一个新的函数 fahrenheit_to_kelvin(),然后将所有的 fahrenheit_to_celsius() 调用更改为 fahrenheit_to_kelvin()。这样,你就明确知道在哪里进行了更改,不必在代码中搜索所有的“温度”提及。

但也要注意,DRY 原则有时可能被过度应用。你并不需要过早优化代码,因此,一个好的备选原则是 WET,即"Write Everything Twice"。当你第三次需要编写某段代码时,试着将其提取为单独的函数或变量可能是一个好方法。随着时间的推移,你会形成自己的判断,知道何时需要复用同样的代码片段(如上述的例子),何时几行重复的代码实际上是不同过程中的一部分。


学以致用


了解杰出的编程实践就像是了解高效的健身方法,只有将这些实践真正运用到编程中,你才能真正获益。

了解所有的肌肉群并不意味着你需要一次性对它们全部进行训练。应当从小事做起:下一次编程时,试着提高你的命名精度。接着,试图编写更简洁版的函数。然后,参考这篇文章,检查你可能忽视的部分。将这视为你每周开始执行的几项健身训练。

记住,我们编写代码不仅是为了让未来的同事更容易理解,更是为了我们自己在未来可以更轻松地理解和维护。我们无需对规则进行教条式的坚守,只需尽可能保持清晰。

你很快会发现,编写简洁的代码需要在代码的精简性和功能性之间进行权衡。例如,如果你在时间压力下写一个一次性的脚本来解析数据,且你确信再也不会有人看到这个脚本,你可以少花一些时间编写清晰的代码。但随着时间的推移,重视代码的可维护性和可读性,不仅能使你更有效地解决问题,也能使你在编程技能上超越许多初级开发者。

当你开始遵循一些基本的软件开发原则,你就已经在从一名硬编码者向一名能够更好地维护和构建软件的专业开发者转变了。

对于提高代码可读性和整洁度,你有什么心得和体会?

推荐阅读:

▶打工人又被 AI “误伤”!印度 CEO:“我裁了 90% 的技术支持团队,都外包给了 AI”

100 美元“骗走” 15% 股份?估值 10 亿美元的创业独角兽 Stability AI 联合创始人起诉 CEO

GPT-4 被曝“变蠢”!为了降本,OpenAI 偷偷搞“小动作

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存