小马的世界

读书笔记-软件工程师指南【2-8-2】编写高质量代码

下面的内容是我在作为一名程序员入职之前阅读的由Gergely Orosz写的The Software Engineer’s Guidebook。我将将阅读时得到的重要的信息总结成中文以供大家分享。

编写高质量代码

作为一位称职的软件开发人员,你会希望编写具有正确抽象层次的代码,能够可靠地运行,考虑到潜在的错误情况。这当然也包括代码的可读性,我们在这一节之前已经提到过这点。

使用正确的抽象层次

在结构化代码时,你会创建抽象的类。这些类将实现细节从代码的其他部分抽象出来。例如,你可能会遇到一个名为“PaymentsIntent”的类,它在一个文件中实现以下功能:

  • 发送API请求进行付款
  • 通过查看JSON中的签名来评估其是否有效,将其转换为PaymentsResponse对象
  • 返回PaymentsResponse响应

这个类并不做很多事情,但你可能会决定将第2个功能——将JSON转换为PaymentsResponse对象——抽象成自己的类。

接着,PaymentsIntent将这样做:

  • 发送API请求进行付款
  • 创建一个新的PaymentResponse对象,然后返回这个对象

为什么我们要抽象出解析付款响应JSON的过程呢?有几个原因:

  • 代码的其他部分现在可以使用相同的功能,而不需要复制逻辑。这符合“不要重复自己”(DRY)原则。
  • 类的责任被缩小,每个类都只有一个责任。
  • 在未来,如果支付API的响应发生变化,更容易确定应该修改哪些代码,因为解析功能集中在一个地方。

在《软件设计哲学》一书中,作者约翰·奥斯特豪特描述了信息隐藏的好处:

“信息隐藏以两种方式减少复杂性。首先,它简化了模块[或类]的接口。接口反映了模块功能的更简单、更抽象的视角,并隐藏了细节;这降低了使用该模块的开发人员的认知负担。其次,信息隐藏使系统更容易演化。如果一部分信息被隐藏了,那么在包含该信息的模块之外就不会有该信息的依赖,因此与该信息相关的设计变更只会影响一个模块。”

以合适的抽象层次构建系统是一个你通过实践不断提升的过程。毕竟,你不希望将系统拆分成太多小块,因为过多数量的微小类会增加不必要的认知负担。

良好地处理错误

据我个人经验,许多故障都可以追溯到代码的错误处理不正确。作为软件开发者,编写代码时需要考虑可能出现的问题,并在错误处理上投入足够的时间和精力。拥有一套一致的错误处理策略很重要。当遇到可能是错误的情况时,你会怎么做?会抛出异常、记录错误、两者兼而或其他方式?你应该能够解释自己是如何处理错误的;最好与团队中的其他人保持一致。记录错误是一个明智的策略,你应该设定记录的策略。其中提供了一个在第五部分:《软件工程》中给出的策略。当怀疑时,使用 防御性编程 。这意味着其他部分代码的输入是不可靠且可能恶意的。在这种思维模式下,你会开始质疑然后验证系统、类甚至函数的输入。以下是一些防御性编程的方法:

  • 验证输入,特别是用户输入。 假设有恶意意图或者意外的不正确信息。例如,你期望一个函数中的一个字符串参数应该是一个正数。但你仍要验证这一点,并在不符合条件时抛出错误。
  • 期望无效的响应。 在调用函数时,不要假设它总是会返回一个“有效”的值。期望空的响应或者奇怪的值。例如,调用一个名为GetSalaryForEmployee()的函数,它返回给定员工的工资数值,验证返回值不是零或者负数,也不是一个不是数字的值。
  • 期望恶意输入。 假设攻击者可能会故意发送破坏系统的输入。例如,攻击者可能会尝试提交可能用于SQL注入攻击的字符串。或者在网络上,攻击者可能会尝试使用跨站脚本(XSS)注入恶意脚本。
  • 期望异常和错误。 在调用函数时,要做好它们可能抛出异常或者返回错误的准备。

有时候你可能不需要使用防御性编程。例如,当使用一个类时,其中所有的输入都经过设计验证。而且,当在强类型语言或者函数声明可能抛出的异常时,你可能不太需要担心防御性编程。语言对错误处理有更多约束时,编译器可以更多地警告不正确的假设。

对“未知”状态要多加警惕

一个令人惊讶的常见问题是API响应与成功或失败代码的映射,以及如何处理“未知”情况。例如,一个用于发起支付请求的支付API可以返回“okay”、“资金不足”或“API暂时不可用”等响应。你需要在代码中将这些响应映射为“成功”或“失败”。你知道支付API可能会后续添加或删除响应,因此你希望构建一个足够健壮的系统,以处理API可能引入一个名为“需要用户操作”的新代码情况。以下是两种常见的做法:

  • 允许列表。 采用此方法,你创建一个成功响应列表(允许列表),假定其他所有响应都为失败。因此,允许列表成为“OK”响应,其他响应都被视为失败。这种方法更为保守。当存在一个不严格属于失败的响应时,问题便会出现,比如“需要用户操作”。
  • 阻止列表。 采用此方法,你创建一个被视为失败的响应列表(阻止列表),假定其他所有响应都是成功的。这种方法更为冒险,因为API可能引入新的错误代码,而你会将它们映射为成功。
  • 处理“未知”状态可能会导致意外问题。 遗憾的是,白名单和黑名单并非通用解决方案。以一个API提供商引入一种以前从未见过的响应代码为例。我知道处理这种未知响应和代码的最佳方法是引入第三个状态(除了“成功”和“失败”之外),即“未知”,并触发错误、警报或其他操作,让工程师查看。请意识到,对未知状态或响应所做的任何决定都可能基于错误的假设。 通常,处理这些的正确方法是不要处理它们,而是抛出一个错误。