对于非常重要的东西,代码可读性令人震惊地定义不明确。我们经常把它作为一揽子规则:使用富有表现力的变量名称。当函数变大时,将其拆分为更小的函数。使用标准设计模式。{#5a3f}

但是我们都看到了遵循规则的代码,但仍然是一团糟。{#6759}

我们可能试图通过堆积更多规则来解决这个问题:如果变量名称变得非常长,那么重构底层逻辑。当一个类积累了大量辅助方法时,也许它应该是两个。不要在不适合的环境中使用设计模式。{#75cb}

指南变成了一个迷宫般的判断调用,需要一个可以挑选的开发人员。换句话说,一个已经编写可读代码的开发人员。{#34da}

所以这里还有另一个难题。为了找到它,我们需要建立一个更广泛的可读性图。{#8ec9}

无论如何,它的可读性是多少? {#8c2f}

在实践中,可读性通常归结为"我喜欢阅读它"。这不是一个很好的启发式方法。除了主观之外,我们过去的阅读体验也会被纠缠在一起。{#32b3}

不可读的代码就像一本试图将自己作为代码传递出去的小说。涵盖长篇叙述评论。包含文本的文件,按顺序读取。为了聪明而聪明。缺乏单词重用。代码试图可读,但它的目标是错误的读者。{#a721}

可读性和代码可读性之间存在差异。{#95d5}

代码创建接口。但代码本身也是一个接口。 {#3c17}

代码看起来漂亮时是否可读?看起来漂亮是可读性的一个很好的副作用,但它并没有那么有用。也许在边缘,它有助于员工保留。但良好的牙科计划也是如此。此外,每个人对"看起来很漂亮"的看法都有不同的看法。很快,这种可读性的定义就会变成一个标签,空格,大括号,骆驼外壳和其他格式化圣战的漩涡。尽管在代码审查过程中引起了关注,但没有人会因为看到错位的论点而晕倒。{#4107}

代码在生成较少错误时是否可读? "减少错误"是好的,但这里的机制是什么?当人们看到可读代码时会有温暖的模糊感受?无论读者对代码皱眉多么有力,阅读都无法召唤出错误。{#1c0b}

当编辑更容易时,代码是否可读?编辑的简易性听起来像是最好的理由。需求更改,功能添加,出现错误,最终有人需要编辑我们的代码。为了在不引起问题的情况下进行编辑,他们需要了解他们正在编辑的内容以及他们的编辑将如何改变行为。这给了我们一个新的启发式:可读代码应该易于安全编辑。{#e5ab}

什么使代码更容易编辑? {#dd99}

在这一点上,我们可能会感到有责任再次敲响规则。 "当变量名称具有表现力时,代码更容易编辑。"很好的尝试,但我们所做的就是将"可读性"重命名为"易于编辑"。我们在这里寻找新的见解,而不是假胡子和假发中的规则记忆。{#ffc3}

让我们首先撇开我们谈论代码的事实。编程已经存在了几十年,是人类历史时间轴上的一个点。如果我们将自己局限于那个点,我们就会从浅井中汲取我们的想法。{#a8d2}

相反,让我们通过界面设计的镜头来看待可读性。我们的生活充满了数字和其他界面。玩具具有使其滚动或发出吱吱声的特征。门有一个接口,可以打开,关闭和锁定。一本书以页面形式排列数据,允许比滚动更快的随机访问。正式的设计培训告诉我们更多关于这些接口的信息;请向您的设计团队咨询更多信息。如果做不到这一点,我们都使用了良好的界面,即使我们并不总是知道是什么让它们变得更好。{#dcb8}

代码创建接口。但代码本身及其IDE也是一个接口。此用户界面面向的是非常少的用户群:我们的团队成员。对于本文的其余部分,我们将其称为"用户",以留在UI设计的顶层。{#70f9}

考虑到这一点,请考虑一些示例用户流:{#46cf}

  • 用户想要添加新功能。要做到这一点,他们必须找到合适的位置并添加功能,而不是添加错误。{#548e} {#548e}
  • 用户想要修复错误。他们需要找到错误的来源并编辑代码,以便它停止发生,而不会引入不同的错误。{#91e0} {#91e0}
  • 用户想要验证边缘情况以某种方式起作用。他们想要找到合适的代码,然后通过逻辑来模拟会发生什么。{#840b} {#840b}

等等。大多数流程遵循类似的模式。我们将看一些易于理解的具体例子,但请记住,我们总是想要记住一般原则,而不是回到规则列表。{#ba6e}


我们可以假设我们的用户无法直接使用正确的代码。这也适用于业余爱好项目;忘记功能的位置很容易,即使我是最初写它的人。所以我们的代码应该是可搜索的。{#579c}

如果我们支持搜索,我们将需要一些SEO。表达变量名称来自此处。如果用户通过从已知点向上移动callstack而无法找到某个功能,则他们可以开始在搜索中键入关键字。现在,并非每个名称都需要包含每个关键字。当我们的用户搜索代码时,他们只需要找到一个入口点,并且可以从那里向外工作。我们需要让他们接近他们想要的地方。包含太多关键字,他们会因嘈杂的搜索结果而感到沮丧。{#867e}

如果用户可以立即说服自己'这种逻辑水平是正确的',他们就能够忘记所有先前的抽象层,为后续层释放心理空间。 {#486D}

用户还可以通过自动完成功能进行搜索。他们大致了解了他们需要调用什么函数或者他们想要使用的枚举案例,因此他们将开始输入并选择有意义的自动完成。如果某个功能仅用于特定情况或需要仔细阅读的警告,我们可以用更长的名称表示。当用户正在阅读自动完成列表时,他们通常会避免看起来很复杂的选项,除非他们知道他们正在做什么。{#47f5}

同样,简短的通用名称可能被视为默认选项,适合临时用户。确保他们不会做任何令人惊讶的事情。我们不应该将setter放入看起来很简单的getter中,因为面向客户的UI不会显示改变其数据的"View"按钮。{#323c} 在面向客户的用户界面中,像暂停这样的常见选项几乎没有文字。随着选项越来越高,文本越来越长,提示用户放慢速度。截图:潘多拉

他们希望以撇脂的速度找到这些信息。在大多数情况下,编译需要时间,并且运行可能需要手动访问许多不同的边缘情况。如果可能,我们的用户更愿意阅读代码的行为,而不是抛出断点并运行代码。{#ab05}

要跳过运行,他们需要满足两个条件:{#e75b}

  1. 他们了解代码正在尝试做什么。{#78ee} {#78ee}
  2. 他们确信它正在做它声称的事情。{#46e7} {#46e7}

抽象有助于满足第一个条件。用户应该剥离抽象层,直到达到所需的粒度级别。按照分层UI的方式思考。我们允许用户首先进行大型导航,然后在他们更接近他们想要详细阅读的逻辑时进行更精确的导航。{#a44f}

顺序读取文件或方法需要线性时间。一旦用户可以通过调用堆栈向上或向下点击,他们就会切换到树搜索。给定均衡的层次结构,只需要对数时间。列表在UI中占有一席之地,但要仔细考虑单个上下文是否需要包含多个方法调用。{#11eb} 当每个菜单中的选项较少时,分层导航会更快。右边的"长"菜单只有11行。我们多久用更多行编写方法?截图:潘多拉

对于第二个条件,不同的用户有不同的策略。在低风险情况下,评论或方法名称可能足以证明。对于风险更高,更复杂的领域,或者当用户被陈旧的评论焚烧时,他们可能会被忽略。有时,即使方法和变量名称也会遭到怀疑。当发生这种情况时,用户必须阅读更多代码并在其脑中保留更大的逻辑模型。小而易于掌握的背景再次得到拯救。如果用户可以立即说服自己"这种逻辑水平是正确的",他们就能够忘记所有以前的抽象层,为后续层释放心理空间。{#5400}

当用户处于此模式时,单个令牌开始变得更重要。 element.visible = true / false bool标志很容易单独解析,但它需要在心理上组合两个不同的标记。相反,如果该标志为 element.visibility =。可见 / .hidden, 涉及标志上下文可以脱脂,而不必阅读变量的名称,找出它是关于visibility.¹我们已经看到了同样的设计改进在面向客户的UI中。在过去的几十年中,确认按钮已从"确定/取消"演变为更具描述性的选项,如"保存/放弃"或"发送/保持编辑"。用户可以通过查看选项本身来找出正在发生的事情,而不是需要阅读整个上下文。{#f468} 一目了然,顶部的"离线模式"横幅显示了我们当前的状态。底部切换传达相同的信息,但仅在查看上下文之后。截图:潘多拉

单元测试还可以帮助我们克服行为证明条件。它们可以作为更值得信赖的评论,因为它们不易受到陈旧性的影响。这仍然涉及构建。但是,任何具有良好CI管道的团队已经运行了测试,因此用户可以跳过现有代码的此步骤。{#6591}


从理论上讲,安全来自于理解。一旦我们的用户理解了代码的当前行为,他们就应该能够安全地编辑它。在实践中,工程师是人。我们的大脑采用与其他人大脑相同的捷径。我们越不了解,我们的行动就越安全。{#9eee}

可读代码应将大多数错误检查卸载到计算机上。调试断言是这样做的一种方式,尽管它们需要构建和运行。更糟糕的是,如果用户忘记了该路径,他们可能无法捕获边缘情况。单元测试可以更好地处理常被遗忘的边缘情况,但是一旦用户进行了更改,它们也需要时间来运行。{#17ba}

简而言之,可读代码必须是可用的。也许,作为副作用,它看起来也很漂亮。 {#7dbe}

为了获得最快的周转时间,我们使用编译器错误。这些很少需要完整构建,甚至可能实时出现。我们如何利用它们?从广义上讲,我们希望寻找编译器变得非常严格的情况。例如,大多数编译器不关心"if"语句是否详尽,但会仔细检查"switch"语句是否存在任何丢失的情况。如果用户试图添加或编辑案例,如果所有先前的条件都是穷举交换机,则它们会更安全。在他们更改案例的那一刻,编译器将标记他们需要重新检查的所有条件。{#5b06}

另一个常见的可读性问题是在条件语句中使用原语。特别是当一个应用程序解析JSON时,很有可能围绕字符串或整数相等来编写大量的if语句。这不仅为拼写错误打开了大门,而且还使用户难以了解哪些值是可能的。在每个字符串都可以检查边缘情况和在可能存在两到三个离散情况时检查边缘情况之间存在很大差异。即使在常量中捕获基元,用户也是一个即将到期的截止日期,而不是分配任意值。如果我们使用自定义对象或枚举,编译器会阻止无效参数并提供有效参数的清晰列表。{#64c7}

同样,如果某些标志组合无效,则优先选择单个枚举而不是多个bool标志。例如,想象一首可以缓冲,完全加载或播放的歌曲。如果我们将其表示为两个bool标志 (加载,播放) ,则编译器允许无效输入 (loaded:false,playing:true) 。但是,如果我们使用枚举, .buffering / .loaded / .playing ,则无效状态甚至不可能。 "禁用无效的设置组合"将是面向客户的UI中的基本功能。但是当我们在应用程序中编写代码时,我们常常忘记给予自己相同的保护。{#4577} 在交互之前禁用无效组合;客户不必考虑哪些配置不一致。截图:Apple

在这个用户流程中,我们已经达到了与开始时相同的规则。但现在我们有了一个生成和定制它们的过程。我们问自己:{#427a}

  • 用户可以轻松找到此代码吗?是否会使搜索结果混乱不相关的功能?{#d621} {#d621}
  • 找到后,用户是否可以快速确认代码的当前行为?{#b103} {#b103}
  • 用户是否可以依靠机器验证来安全地编辑或重用此代码?{#0530} {#0530}

简而言之,可读代码必须是可用的。也许,作为副作用,它看起来也很漂亮。{#2b08}


脚注 {#b07e}

  1. 布尔可能感觉更可重用,但可重用性意味着可互换性。想象一下两个标志, tappablecached 。它们代表完全不同轴上的概念。但是如果两个标志都是布尔值,我们可以在它们之间随意交换,将非平凡的语句("缓存与可执行性相关")隐藏在一小段代码中。使用枚举,只要我们想要形成这种关系,我们就不得不创建明确的,可测试的"单位转换"逻辑。{#ebfe} {#ebfe}

查看英文原文

查看更多文章

公众号:银河系1号

联系邮箱:public@space-explore.com

(未经同意,请勿转载)