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

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

“捷径”这个词表达的能力很强大。这并不是一个捷径。但我认为,这种观点很重要。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 show
end

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 added
addToDate(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

本文文字及图片出自 InfoQ

本文文字及图片出自

余下全文(1/3)
分享这篇文章:

请关注我们:

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注