【ArchSummit架构师峰会】探讨数据与人工智能相互驱动的关系>>> 了解详情
写点什么

怎样编写人们容易阅读的代码?

  • 2021-07-14
  • 本文字数:5164 字

    阅读完需:约 17 分钟

怎样编写人们容易阅读的代码?

有一句广为流传的话:代码是写给人看的,而不是为了给机器执行。然而,编写人类易于阅读的代码,这说起来容易做起来难。这件事需要花费几年时间去学习,花费几十年才能掌握。我也许可以提供一个捷径:像一名教育者一样思考代码质量。


“捷径”这个词表达的能力很强大。这并不是一个捷径。但我认为,这种观点很重要。Alan Kay说过,观点抵得上 80 点智商。

了解你的受众

Rails


在工作中,我们使用 Rails、Node 和 Vue。主应用程序是用 Rails 写的,前端的一部分是用 Vue 写的,然后我们还用 Node 编写了一些 lambda 函数。在理想世界中,Rails 人员写 Rails,Node 人员写 Node,Vue 人员写 Vue,但实际情况并非如此。实际上,我们会遇到这样的情况:Node 和 Vue 人员不得不阅读或编写 Rails 代码。


委婉地说,Rails 是一个古怪的框架,会发生很多奇怪的事情,约定大于配置。例如,如果你看到如下代码:


# app/controllers/api/foo/bar/baz_controller.rb
def showend
复制代码


Rails 会自动在app/views/api/foo/bar/baz内部寻找show.html.erbshow.jbuilder来作为响应。但如果你不是一个 Rails 开发者...你不会知道这些!你所看到的只是一个空方法,它似乎什么也没做!更重要的是,你没办法搞明白。答案不是隐藏在一些父类或 mixin 中,而是藏在这种部落知识的书中。


我不确定这个 controller actions 的示例是不是一个好例子。实际上,这是你很快就能学会的,或者你团队中的某人可以马上发现并帮助你的。但是在其他一些情况下,你可以用 Rails 做一些古怪的事情,而只有那些正好掌握这些部落知识的人能够理解。


当你在一个拥有经验丰富的 Rails 专家的团队工作中时,这不是个问题。事实上,这些古怪的东西能够帮助 Rails 专家变得更高效。但是,如果你工作在一个对 Rails 都是新手的团队中时,这些菜鸟绝对会陷入绝望和沮丧中。


这就是需要像一名教育者一样思考的地方。假设你是一名教授。如果你站在一小群博士面前,举办一个高度专业化、集中化的研讨会,你可以使用花哨的术语等而不用担心这些术语超出观众的认知。但是...如果你发现自己站在一座演讲厅中面对一群本科生,那么,使用这些术语就不是一个明智的选择。


对于 Rails 也是这样。问题不在于某样东西是“最佳实践”还是“Rails 编程方式”,而是在于它对你的受众是否有意义。

Angular


过去犯过这个错误。在以前的一家公司,我们使用 Rails、Angular 和 Python。我是那个“使用 Angular 的家伙”。团队的其他人大部分都是 Rails 人员。


我对于自己对 directives 的使用感到少许得意。但我的上司叫我停止使用这些东西,坚持使用正常的 controllers。他甚至提到,他的理由是因为这是大部分软件开发人员如果进入一个代码库期待看到的编程方式。


当时,我认为很明显,他错了。我看到的 Anguar 专家撰写的所有视频和书籍,都告诉我这些是最佳实践。他们都是专家,而且他们看起来比我的上司更了解 Angular,所以我认为我应该相信这些专家而不是我的上司。至少我是这么想的。

ELI5


在《函数式编程为什么重要》一书中,Eric Normand谈到了一个有关游戏树的程序。他说,在大学里,他用一堆for循环写过一个类似的程序。然后,他谈到了一篇论文作者采取的方法:


他的解决方案,当然更简洁,是...非常简洁。非常简洁。我也不知道我能不能读懂它。


这说明了一些问题,因为 Eric Normand 是一名函数式编程方面的专家。如果你的代码过于简洁,以至于即使是领域专家也很费劲才能理解它,那么这可能不是你应该追求的目标。在同一期播客中,Normand 反思函数式编程语言/代码是否过于简洁。


这让我想起 EliezerYudkowsky 在《Explainers Shoot High. Aim Low!》中写的有些东西:


几年前,一位著名的科学家曾告诉我,他是如何以比平时低得多的技术水平为自己的领域撰写一篇解释性文章的。他认为这对该领域以外的学者,甚至报道者,都会有用。这篇论文最终成为他所在领域最受欢迎的论文之一,被引用次数超过了他所写的其他任何文章。并不是他的同行科学家都很愚蠢,而是我们往往大大低估了正确理解事物所需的努力。


我认为在编写代码时记住这一点是一件好事。

降低水平?


“了解你的受众”并不一定意味着你需要将所有东西都降低水平。


想想大学教授教本科生。在学期开始的时候,可能需要慢慢来,在解释事物时要非常慎重。但是,随着特定术语和概念开始为全班所熟悉,自由地使用这些术语可能就更好。


同样,当有一些术语和概念很难被人们理解时,慢慢介绍这些概念而不是完全避免使用这些概念可能更有意义,这样学生们就可以学习这些概念并在将来使用它们。


我认为问题的关键是,像往常一样,这涉及到权衡问题,你需要意识到这些问题并在你的决策中加以考虑。

可视化


教育者使用什么工具来教学?幻灯片、教科书、讲座视频、演示、测验、办公时间、家庭作业、图表、模拟,等等。当我们编写代码时,这些工具对我们开发者还有使用意义吗?


其中一些是不适用的。例如,一个完整的教学视频。其它则是有点儿傻。例如,测验。但是,我认为至少有些工具是可以使用的。

录像


让我们重新考虑一下讲座视频。对于你写的每个 10 行函数代码,进行一个深入的讲解是不现实的。但是对于更大的代码块呢?对于一个 lambda 函数或者一个重要的模块进行深入的讲解,我认为是有意义的。


事实上,我认为类似的事情已经发生了。当处理代码库中一些自己不太熟悉的部分时,我最喜欢的一个小技巧是使用git blame来增进自己对代码的理解。我会看到大部分代码是谁写的,在 Slack 上交流,然后他们会花费大约 20 分钟时间给我进行大致的讲解。我觉得这非常有用。那么,为什么不像这样记录一份讲解,并在文件头部以代码注释的形式链接到这份讲解呢?


我认为最大的原因是可维护性。随着代码库的演变,视频将变得陈旧过时。当代码注释这样的东西变得过时了,很容易编辑它们,但是对于一个视频,你不能真正地编辑从 17:34 到 21:40 的片段。至少不太容易。


我对这个观点有一些反对意见。是的,它最终会变得陈旧,但那又怎样?如果代码更改很小,视频就仍不会过时,其收益大于成本。如果代码变化很大,那么你可以再花 20 分钟录制一份讲解。即使出于任何原因,团队没有同步,最终导致在代码变化很大时没有更新视频,我也不认为这会造成任何重大伤害。如果有人点击它并开始观看,他们会很快意识到这个视频过时了并停止观看。


另一个我反对的观点是录制视频很费时。那简直是胡说八道。我们已经花费了大量时间来尽量产出高质量的代码:前期工作、重构、代码评审,等等。花 20 分钟时间,以一种随意的意识流的方式对着镜头进行讲解,与你花的其它时间相比是微不足道的。我认为这种观点真正要表达的是,录制视频给人的感觉像是要做一件大事。

图表

好吧,让我们看看其它工具,图表怎么样?我觉得图表很棒!幸运的是,它们已经被一些人采用了。特别是在架构层次,来说明不同的模块是如何连接到另外一个模块的。



然而,我感觉图表仍然没有得到充分利用。


下面是一个例子,说明如何将它用于架构级别较低的事务。对于“水容量最多的容器”问题,以下视觉参考会非常有用:



在我看来,前端代码领域是图表尤其未被充分利用的一个领域。我认为在代码旁边伴随图表很酷,这样你就可以放一张图片展示一个 React 组件是什么样子的。是的,你可能已经通过打开一个网页,并且使用检查工具(或者仅仅通过常识)来确定哪些代码对应哪些 UI,但这样做会有点儿小别扭。也许减少这些小别扭是一个不错的主意。


特别是我想到的以下几点。你的文本编辑器中应该有这个插件。当你的文本编辑器看到一段代码注释后面跟着一个以.jpg结尾的 URL


// https://example.com/code-images/modal.jpg
复制代码


它左边会有一个小的折叠/展开箭头,当你点击展示时,它会内联展示图片!我想这样会方便很多。


我的朋友Brendan Long有一个好主意:使用某种插件根据这些组件的一些模拟数据自动生成这些图表或图片。


总之,这条思路不仅仅是我个人的强烈感觉,更是一种猜想,但确实很有趣!

Clean code

让我们抛开视频之类的边缘话题,回到一个我们更熟悉的世界。在这个世界里,我们尝试以一种更容易被他人理解的方式编写代码。



就像我在文章开头所说的,如果你将自己当作一个教导团队其他人如何使用这段代码的人,很多公认的关于 clean code 的想法都会自然而然地产生。描述性变量名、模块化、恰当的缩进,等等。我还从clean-code-javascript“借”了一些示例过来。


差的代码


// What the heck is 86400000 for?setTimeout(blastOff, 86400000);
复制代码


好的代码


// Declare them as capitalized named constants.const MILLISECONDS_IN_A_DAY = 60 * 60 * 24 * 1000; //86400000;
setTimeout(blastOff, MILLISECONDS_IN_A_DAY);
复制代码


差的代码


const address = "One Infinite Loop, Cupertino 95014";const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;saveCityZipCode(  address.match(cityZipCodeRegex)[1],  address.match(cityZipCodeRegex)[2]);
复制代码


好的代码


const address = "One Infinite Loop, Cupertino 95014";const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;const [_, city, zipCode] = address.match(cityZipCodeRegex) || [];saveCityZipCode(city, zipCode);
复制代码


差的代码


function emailClients(clients) {  clients.forEach(client => {    const clientRecord = database.lookup(client);    if (clientRecord.isActive()) {      email(client);    }  });}
复制代码


好的代码


function emailActiveClients(clients) {  clients.filter(isActiveClient).forEach(email);}function isActiveClient(client) {  const clientRecord = database.lookup(client);  return clientRecord.isActive();}
复制代码


差的代码


function addToDate(date, month) {  // ...}const date = new Date();// It's hard to tell from the function name what is addedaddToDate(date, 1);
复制代码


好的代码


function addMonthToDate(month, date) {  // ...}const date = new Date();addMonthToDate(1, date);
复制代码

代码注释


我比较倾向于传统的日志类注释的写法


/** * 2016-12-20: Removed monads, didn't understand them (RM) * 2016-10-01: Improved using special monads (JP) * 2016-02-03: Removed type-checking (LI) * 2015-03-14: Added combine with type-checking (JR) */function combine(a, b) {  return a + b;}
复制代码


位置标记


////////////////////////////////////////////////////////////////////////////////// Scope Model Instantiation////////////////////////////////////////////////////////////////////////////////$scope.model = {  menu: "foo",  nav: "bar"};////////////////////////////////////////////////////////////////////////////////// Action setup////////////////////////////////////////////////////////////////////////////////const actions = function() {  // ...};
复制代码


明显的注释


function hashIt(data) {  // The hash  let hash = 0;
// Length of string const length = data.length;
// Loop through every character in data for (let i = 0; i < length; i++) { // Get character code. const char = data.charCodeAt(i); // Make the hash hash = (hash << 5) - hash + char; // Convert to 32-bit integer hash &= hash; }}
复制代码


应该避免这样使用位置注释和明显的注释。然而,我对于“好代码通常是它自身的文档”这一点有所保留。我通常默认假定:


  • 我注意到人们在工作中使用解释性注释,而我不会在那些场景中这样做,但我发现这些注释确实很有用;

  • 这种“像教育者一样思考”的框架让我觉得它们很有价值。


我不想争论这些注释是没有被充分利用的。那很难。相反,我只是想建议你据此重新评估自己的立场。下次编写函数时,问问你自己,是否会有其他人会很难理解你所写的代码。问问你自己,是否可以添加一些不会显得多余和臃肿的注释。问问你自己,一名教育者会怎么做。

后记:像个可用性设计师一样思考?


这篇文章是关于你在写代码时像一名教育者一样思考。我认为这个想法很好,但这是唯一的好想法吗?填空:“像个____一样思考代码质量”。还有哪些有意义的想法?


我想到的最重要的是“可用性设计师”。为什么?因为我一直认为,用户测试是人们应该在代码库中做的事情!


想想看,我在文档最后一段所说的:


问问你自己,是否会有其他人会很难理解你所写的代码


可用性设计师一直在做这类事情!这是他们的工作!但不仅如此,他们还做其它哪些事情?


用户测试!他们不会凭空猜想人们会理解如何使用他们的产品。他们会进行测试。把它放到真正的用户面前,看看有哪些别扭的点。为什么我们不能对代码也这样做呢?


原文链接:


https://adamzerner.bearblog.dev/think-like-an-educator-about-code-quality/?fileGuid=rU8e3yc0h4Mztn6T

2021-07-14 16:422558
用户头像

发布了 165 篇内容, 共 71.6 次阅读, 收获喜欢 342 次。

关注

评论 2 条评论

发布
用户头像
// Declare them as capitalized named constants.
const MILLISECONDS_IN_A_DAY = 60 * 60 * 24 * 1000; //86400000;


setTimeout(blastOff, MILLISECONDS_IN_A_DAY);

我不认同这是一段好的代码。MILLISECONDS_IN_A_DAY,命名不该如此,应该更加通用一点,描述的是定时器的作用,而不是直接把时间翻译出来。加入后续要更改值的话,这个变量的命名就会造成误解。
展开
2021-07-14 17:11
回复
没有更多了
发现更多内容

cad看图:MiniCAD 中文版

真大的脸盆

Mac Mac 软件 cad cad看图

SpringCloud 网关实现线程池异步批量保存请求日志

Java你猿哥

spring Spring Cloud Java工程师 日志表

Github发布6天,Star55K+,这套笔记足够你拿下90%的Java面试

做梦都在改BUG

Java java面试 Java八股文 Java面试题 Java面试八股文

技术不行还说Java卷!靠468页SpringBoot企业级项目实战成功逆袭

做梦都在改BUG

Java 微服务 Spring Boot 框架

使用Python实现一个简单的垃圾邮件分类器

海拥(haiyong.site)

三周年连更

阿里巴巴内部Spring Cloud Alibaba 全彩 PDF 版手册开源

采菊东篱下

Java 微服务

【实践篇】基于CAS的单点登录实践之路

京东科技开发者

CAS SSO 单点登录 企业号 4 月 PK 榜

在桌面养只捣蛋鹅 Desktop Goose让你的mac桌面更有趣!

魔仙苹果mac堡

抖音桌面宠物鹅 桌面宠物鸭 Mac版 Desktop Goose怎么关闭 Desktop Goose下载

百度工程师的软件质量与测试随笔

百度Geek说

测试 软件质量 测试技术 智能测试 企业号 4 月 PK 榜

一文解读基于PaddleSeg的钢筋长度超限监控方案

飞桨PaddlePaddle

人工智能 图像识别 飞桨

分享:作业帮在多云环境下的高可用双活架构优化实践

OceanBase 数据库

数据库 oceanbase

什么是接口定义? 接口定义的概念和用途详解

Apifox

前端 接口 后端 API 接口定义

OceanBase 4.1 发版 | 一个面向开发者的里程碑版本

OceanBase 数据库

数据库 oceanbase

从GitHub火到了头条!共计1658页的《java岗面试核心》,拿走不谢

做梦都在改BUG

Java java面试 Java八股文 Java面试题 Java面试八股文

ChatGPT背后的AI背景、技术门道和商业应用(万字长文,建议收藏)

京东科技开发者

人工智能 AI ChatGPT 人工智能ChatGPT 吗? 企业号 4 月 PK 榜

AutoCAD安装无响应,需要在macOS上完全卸载Autodesk产品!

魔仙苹果mac堡

cad2024激活版 AutoCAD安装无响应 AutoCAD M1

Hybrid App 选用什么前端框架更好

Onegun

flutter React Native Hybrid

Spring源码探索-核心原理下(AOP、MVC)

Java你猿哥

spring aop Spring MVC

安全测试前置实践2-安全渗透测试

京东科技开发者

测试 安全测试 功能测试 网络安全渗透测试 企业号 4 月 PK 榜

分享:CUDB for OceanBase分布式数据库产品规模应用

OceanBase 数据库

数据库 oceanbase

CUDA编程基础与Triton模型部署实践

阿里技术

cuda 模型部署

4月22日,云数据库技术沙龙【杭州站】

NineData

MySQL 数据库 开发者 Clickhouse 沙龙预告

Redis分布式锁一定注意两个坑

做梦都在改BUG

Java redis 分布式锁

聊聊简单又不简单的图上多跳过滤查询

华为云开发者联盟

大数据 后端 华为云 华为云开发者联盟 企业号 4 月 PK 榜

横扫一线大厂面试的高并发笔记到底有多硬核?

小小怪下士

Java 程序员 后端 高并发 java面试

数据解析NFT Q1市场表现:NFT生态正向Polygon聚拢,蓝筹项目"保值"难

NFT Research

数据分析 NFT

投放视频广告时,如何快速与第三方播放器兼容?

HMS Core

HMS Core

如何把Ai绘画工具放到我们的App中

Onegun

AI AIGC

Java 源码重读系列之 HashMap

U2647

源码 hash map #java

解密HTTP协议:探索其组成部分与工作原理

做梦都在改BUG

Java 计算机网络 网络协议 HTTP

未来已来,OpenHarmony 3.2 Release发布,迈入发展新阶段

OpenHarmony开发者

OpenHarmony

怎样编写人们容易阅读的代码?_语言 & 开发_Adam Zerner_InfoQ精选文章