不整理代码,上个厕所回来就忘记写到哪里了

368 阅读14分钟

为什么我们需要写整洁的代码?

首先,让我们从一个与编码无关的例子开始来回答这个问题。

假设你刚到朋友家接他们去上班。他们打开门,你立刻看出他们忙得不可开交。也许是睡过头了,现在急忙要出门。他们可能觉得对不起,因为这种情况会耽搁你的时间。你主动提出帮忙。他们让你去他们的房间拿外套。你进入他们的房间,发现地板上堆满了衣服,床上、椅子上和房间其他地方都是。开着的抽屉里挤满了衣服,衣柜里也乱七八糟地扔着。

你会怎么做?

你有几个选择...

  1. 转身说:“不行…”
  2. 去问他们外套放在哪里(希望他们记得)
  3. 开始挖掘…你最终会找到的

现在,想象一下同样的情景,但是房间里一切井然有序。每件东西都整齐地放好了,有一组抽屉,每个都合上并标有标签,“袜子和内衣”,“裤子”,“睡衣”,衣柜里的衣物也整齐挂好了。

现在要找外套会容易多少呢?

这个例子说明了开发人员在处理项目时经历的类似情况。

打开一个新的代码文件就像走进朋友的房间一样。它可以是欢迎的,也可以是令人紧张和不知所措的。一天打开100个文件(甚至更多!),这种紧张和不知所措只会加剧。

查找或更新特定功能或功能片段就像寻找朋友的外套一样。你肯定会找到它,但你会花费多少时间和精力去寻找或弄清楚呢?现在想想你一天或一周里要寻找多少个“外套”。

现在考虑一个更大的混乱。不仅仅是“一个卧室”,而是一个多房间的房子?整个宿舍?或者一个仓库?一个地方有很多房间,很多东西,多个人以不同方式制造混乱?这类似于在一个大型项目上与团队合作,涉及数千甚至数万个文件。有了这个理解,现在让我们回到我们最初的问题……

为什么我们需要写整洁的代码?

简短的答案是……其实我们不需要。就像在你朋友杂乱无序的房间里寻找东西的例子一样,我们仍然可以完成工作。但这是有代价的。项目中完成任务所需的时间增加了。要理解和遵循各种混乱部分需要更多的能量和认知努力。很难知道某件事会花费多长时间,或者需要多大的努力。而且很容易忽视或破坏事物。还有个人的影响。

对我来说,如果我一整天都在混乱、混乱的代码库中工作,为每一个我做的事情投入大量的认知努力,到了一天结束我会感到筋疲力尽,几乎没有精力再做其他事情。而我是那种在空闲时间里非常享受做副业的人。

另一方面,如果我花一天时间在格式整齐、组织良好的代码上工作,我能明显感觉到不同。我感觉没有那么累。我的心情好了,整体感觉也更好。

现在,我没有进行统计显著分析,但至少我已经和其他开发人员谈论过这个问题,似乎很多人也有类似的体验。

此外,我们的大脑是自然的模式识别器,我们无法避免识别出与我们日常环境模式不同的事物。因此,在阅读代码文件时,我们的大脑本能地注意到代码模式的变化。即使是缩进不符合模式,我们的大脑也会分散注意力,关注这种不同,而不是最初的寻找任务(除非你正在寻找无效的缩进,当然 😆)。让我们看一个快速的例子来说明这一点…

<input
    id="my-input"
    type="number"
    class="light large"
    min="0"
        max="100"
    placeholder="Enter a number"
/>

现在,你多快注意到破坏模式的地方?对许多人来说,这是他们注意到的第一件事,也是他们眼睛自然会被吸引的地方。但如果我实际上是在尝试找到class属性以便更新它,这种模式的破坏会让我的注意力从任务中分散开来,即使只是片刻。现在把这种情况乘以一天中数百、数千甚至数万行代码。你能看到当看到“混乱”的代码时,你的大脑可能在进行多少次来回跳动吗?

总之,这些都表明,编写整洁、组织良好的代码更好。

那么,“脏”代码为什么会被写出来呢?

为什么会写出“脏”代码呢?在这部分,我们来看看我听过的三个最常见的理由、原因和借口,并深入探讨一下。也许其中一些听起来很熟悉...

  1. 我赶时间,没时间把它弄整洁。

我理解。在当今的工作环境中,有时候时间表很紧迫。有时候会出现紧急情况,比如一个严重影响销售的 bug,或者阻止某人能够完成工作,我们必须迅速发布修复。或者可能需要尽快发布一个功能,以便与重要客户的交易能够顺利完成。我理解。

然而...

根据我的经验,写整洁的代码其实并不需要花太多时间……尤其是如果在“紧急情况”出现之前已经将其优先考虑了。当我说优先考虑时,我指的是进行一些投资。比如花时间设置代码检查工具和格式化工具。同意(并记录)关于代码结构和组织的基本规则。在持续集成中设置自动化流程。或者将其作为 Pull Request 过程的一部分,要求所有团队成员在合并之前遵循整洁的代码标准。如果事先做了这些投资,当出现紧急情况时,写出的“脏”代码通常也不会太糟糕。也许会缺少一些文档说明。也许命名可以更清晰。或者可能结构可以更好一些。但至少它是可读的,并且与代码库的其余部分基本一致。

  1. 我只是从另一个文件中复制了代码。

我一直认为这就像开发者回答父母问题,“如果你的朋友跳下桥,你会跟着跳吗?”。显然,我们的答案是“会!”

但更严肃地说,这实际上是“破窗效应”的一个很好的例子,该理论指出,“一个未修复的破窗,是无人关心的信号,因此破坏更多窗户并不会有额外的成本。” 这个理论最初关注环境中的混乱如何导致犯罪,并被应用到世界上的许多其他领域。因此,这种辩解就是开发中的“打破窗户效应”,并且正因为另一个窗户(文件)已经破碎,所以可以辩解说这样做是合理的。

那么,我们如何阻止更多的窗户被打破呢?简单...修复第一个“破窗户”。糟糕的代码在代码库中停留的时间越长,引入更多糟糕的代码的可能性就越大。无论是因为复制粘贴,还是因为其他开发人员效仿的例子,都是如此。修复一个窗户可以防止更多窗户被打破。

  1. 我只是想快点搞定,稍后再回来整理它。

哎呀,我多年来听到这个理由太多次了。但悲伤的事实是,“稍后回来”通常是不会发生的。人们往往会转向下一件事情,把“脏”代码留给下一个开发人员去处理。

现在,这种推理可能与我们讨论过的第一个论点(时间不足)相关,但这也可能源于其他原因。我经常看到这种情况发生在任务繁重、开发人员需要处理很多事情时,或者他们长时间在同一件事情上工作,想换个环境做其他事情。不幸的是,由于原因各异,解决方案也会不同。

如果原因与工作负荷或时间有关,我几乎总是建议委派任务。当然,确定哪些任务可以交给谁取决于工作的性质。但总的来说,如果要决定是让开发人员承担当前的工作负荷,并接受因此而产生的“脏”代码,还是花时间重新安排事务,以便开发人员编写“好”代码,我的答案几乎总是后者。

如果原因与动力有关,而时间并不是主要因素,我通常建议从任务中休息一下。去做一些其他事情,帮助恢复动力,然后稍后再回来处理原来的任务。这种方法并不总是可行,但比接受会在未来造成更多问题的混乱代码要好得多。

清洁的代码看起来是什么样子?

我们已经讨论了为什么整洁的代码更好,以及我们可以采取的方法来避免“脏”代码。但实际上,“整洁”的代码是什么样子呢?这是一个非常广泛的主题,一个单一的博客文章无法涵盖所有内容,但如果我能选择一个词来描述这个概念,那个词就是“一致”。

无论你选择遵循哪些规则……是使用2个空格还是4个空格进行制表,是按字母顺序列出类方法还是按功能分组,是将帮助函数放在 /lib、/utils 中,还是完全不允许使用帮助库,等等……重要的是保持一致性。在代码库中打开任何文件时,它都应该遵循一致的模式,这样每个人都能立即知道在哪里查找他们需要的内容,以一种一致的格式。正如我们所见,即使是小的偏差也可能产生复合效应。

我不想深入分析各种规则和方法,也不想引发关于哪种模式或方法更好的争论。但有几点我经常看到的,我认为值得指出,因为它们倾向于不仅加剧我们讨论过的问题,还会引发其他问题。

链式方法和属性格式化

我经常看到的一些情况,尤其是当人们试图快速移动时,是不一致的方法链和 HTML 属性格式化。有些行会有2或3个链接在链中,下一行只有1个,接下来的行又有3或4个。以下是我所说的一些示例:

<button
    id="my-button" class="light large"
    data-testid="my-button"
    type="button" on:click={doSomething} disabled={!processing}>
    Click Me</button>

$output = $example->doSomething()->getSomeData("query")
    ->parseTheData("some property", 999)
    ->logTheData()->getMoreData()->filterTheNewData()->parseTheFilteredData()
    ->doSomethingElse($someVar < 20);

const data = await getData(5, 20).then(res => res.json())
    .then(data => data.myThings)
    .then(data => data.someProp.filter(x => x < 20)).then(data => data.reduce((x, sum) => (x.otherProp + sum), 0))).catch(err => console.log(err));

以下是更好地编写链式方法和属性的方式,要么所有链接都在一行(如果只有很少几个),要么每个链接都在自己的一行上。

这里是上面示例按照这种模式编写后的样子:

<button
    id="my-button"
    class="light large"
    data-testid="my-button"
    type="button"
    on:click={doSomething}
    disabled={!processing}
>
    Click Me
</button>

$output = $example->doSomething()
    ->getSomeData("query")
    ->parseTheData("some property", 999)
    ->logTheData()
    ->getMoreData()
    ->filterTheNewData()
    ->parseTheFilteredData()
    ->doSomethingElse($someVar < 20);

const data = await getData(5, 20)
    .then(res => res.json())
    .then(data => data.myThings)
    .then(data => data.someProp.filter(x => x < 20))
    .then(data => data.reduce((x, sum) => (x.otherProp + sum), 0))
    .catch(err => console.log(err));

这样写更容易阅读,如果我阅读别人的代码,也更容易找到我需要的内容!

函数/方法组织

另一件可能花费大量时间的事情是在文件中搜索特定的函数或方法。有时候你只是想看看有什么可用的。其他时候,你可能正在寻找特定的内容。但如果这些函数或方法没有组织或没有任何顺序,那么找到你需要的东西可能会花费很多时间,或者可能导致你错过或忽视一些东西。

当然,处理这个问题有不同的方法。我偏好按字母顺序排序。但无论你选择哪种方法......我强烈建议你选择一种方法来组织和排序你的函数和方法,并在所有文件中都这样做。这样,无论是你还是其他开发人员在项目上工作,无论是哪个文件,每个人都知道事物的组织方式,能够更快更轻松地找到他们需要的内容。

命名

命名在编码中非常重要,可以节省大量的时间和精力,或者会导致更多的时间浪费和增加的努力。

当命名得当时,阅读代码的开发人员不必寻找其他地方来弄清楚某物是什么或做什么。但当命名不当时,开发人员必须在代码中搜索,首先尝试弄清楚该物是什么或做什么,然后才能继续他们最初的工作......根据代码的复杂性,这可能需要相当多的时间。

让我们看一两个实际代码示例,以说明我的意思:

const user: string;

仅仅通过这个 TypeScript 变量声明,你能知道它保存了什么内容吗?我猜你可能需要查看其他代码,才能弄清楚它实际上是在 UI 中显示的用户名称。

public function users(array $args): array {
  // ...
}

这个 PHP 方法呢?你能在不查看任何其他代码的情况下知道它做什么吗?也许你会猜测它返回一个用户数组......但你会猜到它只返回经过验证的用户数组吗?

无论如何,我可以继续讲下去,但与其这样,我们不如试着积极思考。我们如何才能更好地命名这些东西,以便我们不必为找出这些东西的含义而浪费其他开发人员(以及几个月后我们自己忘记了我们写的东西)的时间呢?为了帮助解决这个问题,我通常有两个建议。

  1. 不要害怕冗长 如果能够准确地传达它们代表什么,那么变量、函数、方法、类等的名字就没有太长。在上面的第一个例子中,一个更清晰的名称可以是像 userNameToDisplay 这样。它比原来的名字长得多,但是这个新的、更冗长的名字不会让人对它的内容产生疑问。无需再去查找其他地方来弄清楚这个变量将保存什么内容。

  2. 将你的名称放入一个句子中 这可能听起来有点傻,但试着在句子中使用你的名称可能会帮助。如果不能很好地放入一个句子中,也许这是重新考虑名称的一个信号。这种做法的几个例子可能包括:

    • “这个变量保存了......”
    • “这个函数用来......”

    如果我们考虑我们上面的例子...

    • “这个变量保存了用户。”

    这个句子是有意义的,但它并没有准确描述变量实际保存了什么。让我们再试一次......

    • “这个变量保存了要在 UI 中显示的用户名。”

    听起来好多了。现在我们把这些词连在一起... userNameToDisplay 或者 userNameToDisplayInTheUI。这两个名称对于阅读代码的任何人来说都非常清楚,变量保存了什么,它的目的是什么。

    让我们再看看第二个例子......

    • “这个函数用来 users。”

    嗯......这真是糟糕。根本没有意义!让我们再试试。

    • “这个函数用来获取 users。”

    这好一些了......但我认为我们仍然可以更清晰些。

    • “这个函数用来从数据库获取经过验证的用户。”

    啊,这就对了!现在我们把这些词连在一起... getVerifiedUsers 或者 getVerifiedUsersFromDB。这两个名称都是优秀的代码示例,没有任何疑问地表明函数的作用......除非你要问的是,“什么是‘经过验证的’用户?”......这是需要理解该函数的额外上下文。这就是注释发挥作用的地方!

总结:

通过本文,我们看到了“脏”代码如何严重影响效率和生产力,以及整洁代码带来的好处。在你的开发生涯中,你可能会形成自己的写作、结构化和组织代码的偏好和观点。保持一致性是写出整洁代码的关键因素之一,这有助于团队协作和项目的持续健康发展。与团队成员沟通,寻找共同点或达成一致意见,是保持项目发展的重要步骤。