好代码,坏代码
代码的特征
- 健壮的
- 可理解的
- 适应需求的
- 将修改点约束在局部
代码质量的定义
-
四个高层目标
-
代码应该能正常工作
-
代码应该持续正常工作
-
代码可能依赖于其他代码,而这些代码会被修改,更新和更换。
-
任何新功能需求都意味着对代码进行修改。
-
我们试图解决的问题也可能随时间的推移而发展:消费者的偏好、业务需求和技术考虑都可能变化。
-
-
代码应该适应不断变化的需求
-
代码不应该重复别人做过的工作
总结来说核心就是减少对外部的依赖以及提高代码的复用性。
-
-
代码质量的六个支柱
- 编写易于理解(可读)的代码。
- 避免意外
- 编写难以误用的代码
- 编写模块化代码
- 编写可重用,可推广的代码
- 编写可测试的代码并适当测试
抽象层次
代码结构是代码质量的根本特征之一,好的结构往往能建立清晰的抽象层次。
层次过厚引发的问题通常比层次过薄引发的问题更严重。如果我们不能确定,最好的办法往往是偏向于薄的层次。
-
代码的层次
函数
好的函数应该想一个短句,如果描述一个函数需要多个语句,或者语句很复杂,那么函数的责任就过重了。
类
类的最重要特征: 内聚性
顺序内聚
功能内聚
关注点分离
接口
定义接口,确定该层次将要暴露的公共函数,是有时用于明确各个层次,以确保实现不会泄露的一种方法。
包,命名空间,或者模块
代码契约
编写代码时考虑如下3件事是有用的
- 对你来说显而易见,但是对其他人并不清晰的事情。
- 其他工程师无意间视图破坏你的代码。
- 过段时间你会忘记自己代码的相关情况。
如果是代码使用要求,不要依靠文档说明,用代码校验它。
代码契约的核心
- 函数名称,或者类的名称,输入参数,输出参数。代码行为预期。
- 前置条件,后置条件,不变量。
- 使用检查与断言强制约束代码的使用条件。
如何处理软件错误
- 系统可恢复错误与不可恢复错误的区别
- 快速失败与大声失败的区别
- 报告错误的不同技术与选用时的考虑因素
如果代码错误无法恢复,则代码中唯一明智的做法就是尝试限制破坏的程度,最大限制的增大工程师注意到的问题并修复的概率。
实际上,代码的编写者是无法确定代码错误是否为可恢复错误的,因为不知道被调用的场景。因此,合理的方法就是报告错误,并上上层代码自己决定是否处理错误。
代码错误时常见的处理方法:
- 失败,由上层代码处理或者让系统崩溃。
- 试图处理错误,并继续执行。
如果代码无法恢复,则在失败时应该立刻报告。记录错误信息。
使用“捕捉各种类型错误并记录而不是将其报告给更高层次的程序”的技术时应该极其谨慎,因为这对调用者来说是非常不好的,调用者根本无法尝试处理错误。
处理错误不好的方法:
- 返回默认值
- 空对象模式
- 抑制错误,实际上什么也不做,但是调用者却错误的认为代码已经执行。
错误的报告方式:
- 显示报告
- 隐式报告
无法恢复的错误应该使用隐式错误处理方法。
编写易于理解的代码
- 使用使代码不言自明的技术。
- 确保代码中的细节对他人来说是清晰的。
- 以正当的理由使用语言特性。
使用描述性的名称
- 变量,函数和类的含义不言自明
- 即使单独查看,各段代码也清晰。
适当使用注释
- 解释代码完成的是什么。
- 解释代码为什么完成这些工作。
- 提供其他信息。如使用指南。
坚持一致的代码风格。
避免深层次的嵌套。
使函数的调用易于理解
- 使用命名参数
- 使用描述性参数类型
- 避免使用特殊的常数值,可以定义为常量或者函数返回值。
如何避免代码是意料之外的
- 避免返回魔法值。
调用者应该确却知道函数会返回什么,如果特殊情况返回一个完全没考虑到的值,代码就很容易有问题
空对象模式是无法取得某个值时返回空值(或空的可选值)的替代方法。这种方法的思路是,不返回空值,而是返回一个导致下游逻辑以无害方式运行的有效值。最简单的形式是返回一个空字符串或一个空列表,更复杂的形式则涉及实现一整个类,其中每个成员函数要么不做任何操作,要么返回一个默认值。
- 是否返回空值或者默认值取决于在当前系统中是否合适。
- 避免意料之外的副作用
- 向用户展示输出
- 将某些文件保存到文件或者数据库中
- 调用另一个系统,产生一些网络流量
- 更新缓存或使之失效。
- 当多个函数需要处理输入参数时,不要在中间某个环节去修改输入参数。否则就会造成混乱,因为你不知道是谁
- 避免编写误导性代码
编写难以误用的代码
接口设计原则
API和接口应该“易于使用、难以误用”
当一些假设不直观或者模糊不清,又没能阻止其他工程师做错事时,代码往往很容易被误用。代码被误用的一些常见方式如下:
- 调用者提供无效的输入
- 其他代码的副作用(如修改输入类)
- 调用者没有在正确的时机或者以正确的顺序调用函数
- 修改相关代码时破坏了某个假设
考虑使用不可变值
实现深度不可复制性
- 防御性复制
- 使用第三方库的不可变数据结构
尽可能使用专有类型,因为通用的数据类型,意味着什么参数都接收。更可能被误用。
提高代码的模块性
模块化的主旨之一是我们应创建可以轻松调整和重新配置的代码,而无须准确地知道如何调整或重新配置这些代码。实现这一点的关键目标之一是,不同功能(或需求)应该映射到代码库的不同部分。如果我们实现了这一点,随后有一项软件需求更改,我们应该只需要明显更改代码库中与该需求或功能相关的单一位置。
- 考虑使用依赖注入
- 倾向依赖接口
继承可能造成的问题
- 无论你是否需要,继承的关键特征之一是,子类将继承超类提供的所有功能。
- 继承性可能妨碍清晰的抽象层次
- 继承性使代码难以适应不同情况
类应该只关注自身也就是所谓的最少知道原则
- 防止在返回类型中泄露细节
- 防止在异常中泄露细节
提高代码的可复用性
即使某个子问题的解决方案已经存在,也并不总意味着我们能够重用它。如果解决方案做出的假设不符合我们的用例,或者与我们不需要的其他逻辑绑定在一起,就会出现这种情况。
-
注意各种假设
-
如果假设是必须的则强制要求
-
避免使用全局状态
-
恰当的使用默认值
将使用默认值的相关决策切换到调用者手中往往使代码更容易重用。但在不支持空值合并运算符的编程语言中返回空值,将迫使调用者编写重复的代码来处理它。
单元测试
-
代码的可测试性,以及单元测试应该在一开始就得到考虑,而不是在代码编写完才去考虑。
-
好的单元测试拥有的特征
-
准确检测到破坏
如果代码遭到破坏,测试应该失败,而且,测试应该只在代码确实被破坏的时候失败。
-
与实现细节无关
理想情况下,测试细节的改变不会改变测试代码的改变。
-
充分解释失败,如果代码被破坏应该得到清晰的信息。
-
测试代码容易理解
-
测试代码运行成本低
-

浙公网安备 33010602011771号