代码整洁之道
代码整洁之道
- 代码猴子(Code Monkey): 低水平编码者.
- 童子军规.
- 技艺(craftsmanship):
- 知和行. 学写整洁代码, 掌握原则和模式, 并付出行动.
整洁代码
- 代码呈现了需求的细节. 这些细节无法被忽略或抽象, 必须要严谨, 精确, 规范和详细.
糟糕的代码
-
糟糕的代码可能毁掉一家公司.
-
稍后等于永不(Later equals never).
-
随着混乱的增加, 团队生产力也持续下降吗最后将趋向于零.
-
花时间保持代码整洁不但有关效率, 还有关生存.
-
代码整洁的程序员就像是艺术家, 它能用一系列变换把一块白板变作由优秀代码构成的系统.
-
在写整洁代码时, 要遵循大量的小技巧, 贯彻刻苦学习整洁感.
-
敷衍了事的错误处理大麦知识程序员忽视细节的一种表现:
- 内存泄漏;
- 竞态条件代码;
- 前后不一致的命名方式.
- 整洁的代码从不隐藏设计者的意图. --- 干净利落的抽象和直截了当的控制语句.
-
简单代码, 依其重要性:
- 能通过所有测试.
- 没有重复代码.
- 体现系统中的全部设计理念.
- 包括尽量少的实体, 比如类, 方法和函数等.
-
有意义的命名是体现表达力的一种方式, 需要修改好几次才能确定下名字.
-
如果一个对象功能太多, 最好是切分为两个或多个对象.
-
如果方法功能太多, 使用抽象功能重构.
-
消除重复,提高表达力, 提早构建简单抽象, 在代码整洁方面很有用.
-
读与写花费的时间的比例超过10:1.
-
单一权责原则, 开放闭合原则和依赖倒置原则.
有意义的命名
- 变量, 函数, 参数, 类和封包命名.
- 选一个好名字花时间, 但省下来的比花掉的更多.
- 如果名称需要注释来补充, 就不算是名副其实了.
- 上下文在代码中要明确的体现.
避免误导
-
避免使用与本意相悖的词.
-
提防使用不同之处较小的名称. 误导性的名称真的很可怕.
-
做有意义的区分:
- Info和Data就像a, an, the一样, 是意义含混的废话.
-
使用读得出来的名称:
- 尽量用英语单词(长点也没有关系).
-
使用可搜索的名称:
- 名称长短应该与其作用域大小相对应. 变量或常量存在代码中多处使用, 应赋予其便于搜索的名称.
-
避免使用编码:
- 带编码的名称通常不便于发音.
- 不必用m_前缀来标明成员变量, 应当把类和函数做得足够小, 消除对成员前缀的需要.
- 代码阅读得越多, 眼中就越没有前缀.
- 接口和实现, 尽量区分.
-
避免思维映射:
- 除了循环计数可用i, j, k, 其他地方尽量不用.
- 明确就是王道. 编写其他人能理解的代码.
-
类名:
- 类名和对象名应该是名词或名词短语.
-
方法名:
- 方法名应该是动词或动词短语.
-
命名应该言简意赅, 不要使用俚语.
-
给每个概念对应一个词:
- 给每个抽象概念选一个词, 并一以贯之.
-
别用双关语:
- 避免将同一单词用于不同目的, 同一术语用于不同概念. 一词一意.
-
使用源自所涉问题领域的名称.
- 优秀的程序员和设计师, 其工作之一就是分离解决方案领域和问题领域的概念.
-
添加有意义的语境:
- 用良好命名的类, 函数或名称空间来放置名称, 给读者提供语境.
- 将隶属一个对象的名称封装为一个类.
-
不要添加没用的语境:
- 会混淆理解.
-
取好名字最难的地方在于需要良好的描述技巧和共有的文化背景.
函数
- 短小: 函数的第一规则就是短小. 第二规则是还要更短小.
- 函数20行封顶最佳.
- 每个函数只说明一件事情, 而且依序带到下一个函数.
- if语句, else语句, while语句等, 其中的代码块应该只有一行. 这行应该是函数调用语句.
- 块内调用的函数拥有较具说明性的名称.
- 函数的缩进层次不应该多于一层或两层.
- 函数应该做一件事, 做好这件事, 只做一件事.
- 如果函数只做了该函数名下同一抽象层上的步骤, 则函数还是只做了一件事.
- 编写函数的目的是为了把大一些的概念拆分为另一抽象层上的一系列步骤.
- 只做一件事的函数无法合理地切分为多个区段.
- 每个函数一个抽象层级:
- 确保函数只做一件事, 函数中的语句都要在同一抽象层级上.
- 函数中混杂不同抽象层级, 往往让人迷惑, 读者无法判断是基础概念还是细节. 基础概念不能和细节混杂.
- 自顶向下读代码: 向下规则.
- 每一段都描述当前抽象层级, 并向下后续.
- 确保每个switch语句都埋藏在较低的抽象层级, 而且永远不重复. --- 利用多态来实现.
- 将switch语句埋在抽象工厂的底下, 不让任何人看到. 抽象工厂使用switch语句创建适当的实体.
- 使用描述性的名称:
- 长而具有描述性的名称, 要比短而令人费解的名称好.
- 命名方式要保持一致, 使用与模块名一脉相承的短语, 名词和动词给函数命名.
- 函数参数:
- 最理想的参数是0, 其次是1, 应尽量避免三参数的函数.
- 参数与函数名处在不同的抽象层, 参数越多, 测试用例也会很难写.
- 输出参数比输入参数更加难理解.
- 一元函数的普遍形式.
- 如果函数有三个或三个以上参数, 就说明其中一些参数应该封装为类了.
- 函数的关键字形式, 把参数的名称编写成函数名.
- 应避免使用输出参数, 如果函数必须要修改某种状态, 那就应该修改所属对象的状态.
- 分割指令与询问:
- 函数要么做什么事, 要么回答什么事, 但二者不可兼得.
- 使用异常代替返回错误码:
- 如果使用异常代替返回错误码, 错误处理代码就能从主路径代码中分离出来.
- try/catch代码块丑陋不堪. 会搞乱代码结构.
- 处理错误就是一件事.
- 别重复自己:
- 重复会呆滞代码臃肿.
- 重复是软件中一切邪恶的根源.
- 许多原则和实践规则都是为控制与消除重复而创建.
- 面向对象编程, 将代码集中到基类, 为了避免冗余.
- 面向方面编程, 面向组件编程也都是消除重复的一种策略.
- 结构化编程:
- 每个函数, 函数中的每个代码块都应该有一个入口, 一个出口.
- 在每个函数中只有一个return语句, 循环中不能break或continue, 永远不能goto.
- 只要函数保持短小, 偶尔return, break, continue是没有坏处的. 甚至比单入单出更具有表现力.
- 先写初稿, 然后不断打磨这些代码, 分解函数, 修改名称, 消除重复.
注释
- 如果长于用语言来表达意图, 那么就不需要注释.
- 注释的恰当用法是弥补在用代码表达意图时遭遇的失败.
- 如果发现需要写注释, 就再想想有没有更好的办法用代码来表达.
- 注释不能美化糟糕的代码. --- 应该用代码来阐述.
- 好注释:
- 有的注释是必须的, 法律信息, 提供信息的注释, 对意图的解释, 阐释, 警告, TODO注释, 放大不合理性.
- TODO形式在源码中放置要做的工作列表.
- TODO是一种程序员认为应该做, 但由于某些原因目前还没有做的工作.
- 坏注释:
- 喃喃自语, 多余的注释, 误导性注释, 循规式注释, 日志式注释, 废话注释, 可怕的废话, 能用函数和变量时就别用注释, 位置标记, 括号后面的注释, 归属与署名, 注释掉的代码, HTML注释, 非本地信息, 信息过多, 不明显的联系, 函数头.
格式
- 代码格式和重要, 代码格式不可忽略, 必须严肃对待.
- 保证代码的可读性.
- 垂直格式:
- 源文件名称要简单且一目了然.
- 代码从上至下, 从左到右, 每组代码展示一条完整的思路, 这些思路用空白行区隔开来.
- 垂直方向上的靠近: 精密相关的代码应该相互靠近.
- 垂直距离: 变量声明应该尽可能靠近其使用位置.
- 本地变量应该在函数的顶部出现(函数很短的情况下).
- 实体变量应该在类的顶部声明. C++采用剪刀原则.
- 相关函数: 若某个函数调用另外一个函数, 应该把它们放在一起, 而且调用者应该尽可能放在被调用者上面.
- 概念相关的代码应该放在一起, 相关性越强, 彼此之间的距离就应该越短.
- 横向格式:
- 一般为80字符, 需遵循无需滚动条到右边的原则. 最大上限为120个字符.
- 水平方向的区隔与靠近:
- 赋值操作符周围加上空格字符, 以表强调.
- 不在函数名和左圆括号之间加空格. --- 函数与其参数密切相关.
- 按运算符优先级加空格.
- 尽力不对齐一组声明中的变量, 和赋值语句中的右值.
- 源文件是一种继承结构, 而不是一种大纲结构. --- 缩进让代码的可读性更好.
- 团队规则:
- 每个程序员都有自己喜欢的格式规范, 但在一个团队中工作, 就是团队说了算.
- 好的软件系统是由一系列读起来不错的代码文件组成的. --- 需要拥有一致和顺畅的风格.
对象和数据结构
- 将变量设置为私有(private)有一个理由, 不想让其他人依赖这些变量.
- 数据抽象:
- 隐藏实现关乎抽象.
- 不愿暴露数据细节, 更愿意以抽象形态表述数据.
- 数据, 对象的反对称性:
- 对象把数据隐藏于抽象之后, 暴露操作数据的函数.
- 数据结构暴露其数据, 没有提供有意义的函数.
- 过程式代码便于在不改动既有数据结构的前提下添加新函数; 面向对象代码便于在不改动既有函数的前提下添加新类.
- The Law of Demeter:
- 模块不应了解它操作对象的内部情形. 对象隐藏数据, 暴露操作.
- 方法不应调用由任何函数返回的对象的方法.
- 尽量不使用连串的调用.
- 数据传送对象:
- 只有公共变量, 没有函数的类, DTO(data transfer objects).
- Active Record, 拥有公共变量的数据结构, 但也拥有save和find这样的方法.
错误处理
- 使用异常而非返回值, 在C++中是有些争议的.
- 先写Try-catch-Finally语句.
- try代码块就像是事务, catch代码块将程序维持在一种持续状态.
- 使用不可控异常:
- 可控异常需要捕获, 使用成本较高.
- 异常发生的环境说明.
- 依调用者需要定义异常类.
- 定义常规流程. 在业务逻辑和错误处理代码之间要有良好的区别.
- 不要返回null值. --- 也不要传递null值.
边界
- 使用第三方代码:
- 第三方程序包和框架提供者追求普适性.
- 避免从公共API中返回边界接口, 或将边界接口作为参数传递给公共API.
- 学习性测试的好处.
单元测试
- TDD 三定律:
- 在编写不能通过的单元测试前, 不可编写生成代码.
- 只可编写刚好无法通过的单元测试, 不能编译也算不能通过.
- 只可编写刚好足以通过当前失败测试的生产代码.
- 测试代码和生产代码一样重要, 它需要被思考, 被设计和被照料, 应该像生产代码一样保持整洁.
- 可读性, 明确, 整洁, 还有足够的表达里.
- F.I.R.S.T原则:
- 快速(Fast), 测试应该足够快.
- 独立(Independent), 测试应该相互独立.
- 可重复性(Repeatable), 测试应在任何环境中重复通过.
- 自足验证(Self-Validating), 测试应该有布尔值输出.
- 及时(Timely), 测试应及时编写.
类
- 类应该短小:
- 类的短小用权责(responsibility)来衡量.
- 单一权责原则(SRP) --- 类或模块应有且只有一条加以修改的理由.
- 系统应该由一些短小的类而不是巨大的类组成, 每个小类封装一个权责, 只有一个修改的原因, 并与少数其他类一起协同达成期望的系统行为.
- 类应该只有少量实体变量, 方法操作的变量越多, 就越粘聚到类上.
- 内聚性高, 意味着类中的方法和变量互相依赖, 相互结合成一个逻辑整体.
- 将一些变量和方法拆分到两个或多个类中, 让新的类更为内聚.
- 保持内聚性会得到很多短小的类.
- 为了修改而组织:
- 需求会改变, 所以代码也会改变, 具体类包含实现细节, 而抽象类则值程序概念.
系统
- 软件系统应该将启动过程和启动过程之后的运行逻辑分开, 在启动过程中构建应用对象.
- 分解main.
- 使用抽象工厂模式让应用程序控制何时创建对象.
跌进
- 通过跌进设计达到整洁目的.
- 尽可能少的类和方法, 保持整个系统的短小精悍.
并发编程
- 编写整洁的并发程序非常难.
- 并发防御原则:
- 单一权责原则:
- 并发相关代码有自己的并发, 修正和调试生命周期.
- 并发相关代码有自己要对付的挑战, 和非并发相关代码不同.
- 限制数据作用域.
- 使用数据副本.
- 线程应尽可能地独立.
- 单一权责原则:
- 并发编程的模型:
- 生产者-消费者模型.
- 读者-消费者模型.
- 哲学家问题.
- 警惕同步方法之间的依赖.
- 保持同步区域尽量小.
逐步改进
- 徐徐渐进地改进代码.
- 首先让程序能够正常工作, 再让它做对, 接着想办法让它简单整洁.