序
:::info ℹ️ 书籍摘录 from《代码整洁之道 CleanCode: A handbook of Agile Software Craftmanship》 :::
试问什么是整洁的代码?
面向「人」写代码。
WIP
命名
- 名副其实,减少缩写
- 避免误导,比如 o 和 0
- 做有意义的区分,一看就能够懂意思
- 使用可搜索的名称,单一工程下尽量去除重复命名
- 尽量避免使用没有意义的编码,比如成员前缀等等
I
前缀的滥用就是反例
- 类和实例使用名词,方法使用动词或者动词短语
- 一个概念对应一个词
- 使用领域解决方案的专用词汇
- 设计模式
- CS 领域相关词汇
- 使用涉及要解决特定的领域问题的专用词汇
- 增加有意义的修饰词,以提供语境
函数
- 短小、简单
- 职责单一,只做一件事情(看起来容易,很多时候贯彻落实却很难)
- 每个函数都是不同层次的抽象
- 函数参数数量最小化原则,少即是多
- 单参函数,双参函数尽量作为常态使用
- 长函数参数,应该 对象化
- 杜绝标志性参数,典型的就是
boolean
- 函数尽量减少「副作用」,理想的函数应该是纯函数
- 函数内部尽量使用「异常」来代替错误码
- 尝试将
try-catch
在调用层隔离,不要在函数体内实现 - 永远别尝试重复自己,消灭重复!
- 不断 重构 的心
注释
- 代码即注释
- 较多的注释并不意味着一定就好,因为很多时候没有及时更新的注释具有「欺骗性」
- 注释并不能「美化」糟糕的代码,反而会引入理解成本问题
- 值得「注释」的地方
- 法律信息
- 提供信息的注释 & 阐释(换一种语言描述助于理解)
- 对意图的解释(Why)
- 警告 WARN
- 待办 TODO
- 放大结构,利用注释来放大不合理的代码
- 公共 API 值得类似 JavaDoc 的方式提供完备的注释体系
- 不值得或者糟糕的「注释」
- 描述代码实现的注释(How)即废话
- 纯粹的废话注释,比如:
/* Class Constructor */
- 无关的内容(喃喃自语)
- 私有 API 无关的正式 Doc 或者高频变化的实现的正式 Doc
- 日志式注释(增加各种版本信息 bla … bla … bla …)
- 位置标记(分隔符)大部分不建议做,除非函数体内确乎需要隔离,但这有悖于函数最小化的原则
- 归属签名(意义不大)
- 注释掉的代码一定不要留,除非有特定明确意图保留的说明
- 信息不要过多,过多信息可以外链出去
@see or @link
格式
用好 Linter 和 Formatter
- 代码格式在当代多人协同的团队中显得非常重要
- 垂直格式
- 单个文件 300 ~ 500 行或许比较合适(统计给出)
- 合理使用空白符或者分隔符切分内容(设计上叫做留白)
- 垂直对齐
- 结构体内顺序做适当设计,比如
private
和public
成员顺序
- 横向格式
- 120 字符左右比较适合现代显示器 🖥
- 水平对齐、缩进
- 分号的使用
- 建立团队的规则,并用工具保证的同时做卡口
对象和数据结构
- 对数据对象,务必做好封装和抽象,面向接口、getter / setter 设计。
- DTO(Java 中数据传送对象)就是一个典型。
- 面向过程 & 面向对象的非对称性:过程式代码难以添加新的数据结构,因为必须修改所有关联函数;面向对象式的代码难以添加新的函数,因为必须修改所有关联的类。
- The Laws of Demeter:模块不应该了解它所操作对象的内部情形(细节)。
错误处理
- 使用异常而非错误码,具备直观性。
- 对于可能存在异常的代码,先编写
try-catch-finally
语句,便于自身理解逻辑,定义结构范围。 - 给出异常发生的环境信息,比如
stack-trace
。 - 异常类一定是服务于调用者的,设计的时候务必按照调用者的需要进行设计。
- 谨慎对待 null 值,最好不要返回或者传递 null 值。
边界
创造整洁的边界。
- 谨慎使用第三方的代码,对于三方代码抛出的类型,我们应该进行封装,不要在自己的模块中对其它模块产生直接依赖,否则三方的代码升级或者变更会导致一系列链式反应。
- 使用「学习性测试」来调用第三方代码,增进对三方代码的理解。
- 对于黑盒式的代码,使用适配器模式(Adapter)做好隔离,实现整洁的边界。
单元测试
- TDD 三定律:
- 在编写不能通过的单元测试前,不可编写任意代码
- 之可编写刚好无法通过的单元测试,不能编译也算不能够通过
- 之可编写刚好足以通过当前失败测试的代码
- 保持测试代码的整洁度,它一样重要,不能够被当做二等公民来对待。
- 对于特定的复杂度的项目,我们可以使用 DSL 编写测试。
- 尽量做到一个测试,一个断言,「单一职责,如是而已」。
- 每个测试函数也应该尽量做到测试一个概念。
类
- 成员类型的组织顺序需要有所讲究。
- 类应该短小(SRP),持续重构,防止类变得臃肿无助。
- 高内聚、低耦合。
- 为方便于修改和扩展而设计(IoC),最好能够实现类之间的正交关系。
:::info ℹ️ 类,这块,在《重构》这本书里面,提到了大量的最佳实践,值得深入。 :::
系统
- 更高的抽象等级,产生组件、模块等更高维度的抽象概念。
- 充分使用构建对象的设计模式
- builder
- factory
- 用好依赖注入,便于大型项目的依赖管理
- 考虑扩容的情形
- 考虑代理模式
- 使用 AOP 编程进行模块化
- DSL 的使用可以简化部分复杂的设计,让领域专家能够和领域特定的语言打配合,简化设计和实现
- css & html 都是典范
- 适度采用测试驱动的系统架构(TDD 在系统层的实践)
迭进
- 运行所有的测试
- 重构
- 消除重复
- 增强表达力
- 减少类和方法
并发编程
对象是对过程的抽象,线程是对并发的抽象。 —— James O Coplien
- 能够减少使用并发的场景就尽量避免,毕竟编写整洁的编发程序很难,非常难。
- 并发的防御原则
- 单一职责原则 SRP:将并发的逻辑和业务的实现细节分离。
- 限制数据作用域:Java 中提供了
synchornized
关键字来在代码中保护一块使用共享对象的临界区(critical section)。 - 使用数据副本:线程自治自己的副本,后使用单线程进行数据合并。
- 线程应该尽可能独立:减少和其它线程的通信和耦合。
- 典型的 case
java.util.concurrent
包中的线程安全群集- 生产者和消费者模型:一个或者多个生产者线程创建默写工作,并放置于缓存队列中。一个或者多个消费者线程从该队列中获取并完成这些工作。
- 读者 - 作者模型:平衡读者和作者的 I/O & 吞吐量,避免饥饿。锁模型会在这里产生重要的功效。
- 哲学家宴席问题:死锁问题,参考业界成熟算法,做好规避。
- 警惕同步方法之间的依赖,并保持同步区域微小。
- 测试多线程的关注要素:
- 将「不可能失败」的失败,往往可以归结到线程问题上。
- 使用非线程代码依旧可以 work 的原则。
- 进一步:编写可插拔的线程代码。
- 编写可调整的线程代码。比如线程数、吞吐量可以自适应调整。
- 支持跨平台(可移植性问题)。
- 尝试使用硬编码和自动化。