代码整洁之道学习笔记

253 阅读12分钟

整洁代码的目的

是为了使代码更具可读性和可拓展性

总体有三个基调

  1. 随着系统的维护,代码可以进行不断的优化
  2. 优化的方式有多种,并没有固定正确的一种
  3. 当整洁代码的理念和强制性的标准冲突时,遵循强制性标准

总体目录

image.png

具体内容

命名

好的命名

  • 常量和变量:使用名词和形容类的短语,如userData。如果是对象、数字或者字符串,直接描述,如name;如果是bool值,则是一个回答true/false的词,如isActive
  • 函数和方法:使用动词和形容类的短语,如sendMsg。如果表示一个操作,直接描述该操作,如getUser;如果返回是bool类型,则是一个回答true/false的词,如isActive
  • :使用名词和名词类短语,如Requestbody。直接描述即可,但是不要太明确类,尤其对于父类,要注意抽象层级

命名规则:

一般有四种:

  • 蛇形:is_vaild <一般在python中用>
  • 小驼峰:isVaild
  • 大驼峰:IsVaild
  • 横线:is-vaild <一般在html中用>

具体原则:

  • 望词知意
    • 不应借助注释来解释名字的含义
    • 不要使用魔数,定义成const值
    • 不使用双关语
    • 使用只有程序员能看懂的专业领域词汇
    • 只要短名字能描述清楚的,就比长名字要好
  • 可搜索的命名
    • 避免使用类似a、b、i、j、tmp等无意义的名称
    • 上下文使用统一的词汇,比如get、fetch等同义词,上下文使用一种
    • 可以合并到类中的函数,就合并到类中,比如getUsername(),合并后作为user.getName

函数

描述函数

  • 使用描述详细直观的函数名和参数。不使用短的含义不明的描述

函数的准则

  • 尽可能的短小,但也看实际情况
  • 每个函数只做一件事情
    • 一个函数中不应拆分出其他的新函数
    • 函数内部不应有段落的区分
    • 无副作用,一个函数要尽量避免主逻辑外的副作用(也是看情况),预料外的副作用一定是坑
    • 分隔命令和查询,一个函数要不就做一些事,要不就查询一些事情。尽量不要两者都干。
  • 每个函数中的抽象层级应该尽可能一致
    • 尽可能不要既有高抽象层级(人为抽象为一个函数),又有低抽象层级(语言中自带的方法)的混用。
    • 自顶向下的顺序,被调用函数在调用函数的下面。
    • 避免无意义的抽象,不要为了抽象而强行抽象。
  • switch配合多态
    • switch本身就用于选择各种情况,所以主流程中引入必然会比较臃肿,把switch的逻辑抽象出来
    • 有多态特性的语言(动态多态),将switch语句和工厂模式做封装,代码中动态加载函数实现
  • 函数参数
    • 参数个数越少越好,0、1、2都可以。但是尽量避免超过3个,不好理解,且顺序容易搞错。超过3个(包含3个),应该封装成对象传进去。
    • 不要在参数中传递bool值
    • 尽量不要使用传出参数。使用改变的对象状态或者返回值来代替穿出参数
  • 不要重复自己
    • 有重复的代码就提炼出一个函数来
  • 使用异常而不是错误码
    • 错误处理只做一件事情,不要多做。

注释

总体思想

  1. 软件中最有用的是代码而非注释
  2. 注释不能美化代码,与其多花时间写注释,不如多花时间整理代码
  • 好注释
    • 法律信息
    • warning和todo
    • 代码逻辑不合理的解释(更建议去整理代码)
    • 开放api的解释
  • 坏注释
    • 自言自语
    • 多余的注释:代码能表达清楚就不要注释了
    • 误导性的注释:更新代码记得更新注释
    • 日志性的注释:当成日志来写的注释
    • 注释掉的代码:应该直接删除掉

格式

  • 水平格式:
    • 使用缩紧,大部分ide会自动缩进
    • 使用空格,使代码更具可读性
    • 该换行就换行
  • 垂直格式:
    • 高层次的概念和函数放到文件的顶部,低层次的细节和函数放到下面
    • 不同概念要用隔开,相近的要靠近。减少读者的翻页频率
    • 垂直距离
      • 变量声明的地方要靠近使用的地方
      • 相关函数、相关概念放在一起
      • 被调用者放到调用者的下面
  • 团队规则
    • 一个团队一套代码规则

真实对象和数据容器

  1. 真实对象:要尽可能的隐藏细节,调用方不关心内部细节,只调用暴露的公共api
  2. 数据容器:内部的数据都是公开的,不抽象公共的api,只用来存储数据或者传参数
  • 对于真实对象的类,在不改动现有struct的前提下,可以直接增加新的函数
  • 对于数据容器的类,通过多态,增加新的类
  • 如果要将多个属性封装成类,那么实例化时应该有实例可以做的事情,而不仅仅只是返回类内部的数据结构
  1. 不要火车式的调用ctx.getOption().getDir().getPath()。

错误处理

  • 使用异常而非错误码:主流程中大量的错误码会使正常流程和异常流程相耦合,大量错误码对以后维护不友好,在主流程中及时抛出错误。
  • 全面的error信息:error信息要全面,方便快速做定位
  • 定义异常类:使用统一的错误处理模块,可以通过多态特性根据不同的调用者定义不同的error类
  • 尽量减少null的返回和传递,否则都要判断if null的情况。
  • 对于要判断是否为空的参数,在传参的时候可以给一个默认参数,取消判断逻辑

边界

  • 第三方代码:
    • 提供给别人用的第三方代码和框架要有普适性。
    • 不要直接传递map的实体,而是通过类暴露的接口来获取map的值。
  • 学习性测试:
    • 当引入第三方包时,对用到的所有接口写测试加深理解,不但可以在第三方包升级时,测试改动对我们是否有影响(包括预期行为和兼容性),还可以将包封装成自己的类,自定义边界。
  • 代码的边界:
    • 定义好代码的边界,对于依赖还没到位api,可以直接mock出一个来。依赖好了之后再替换
    • 可以使用adapter模式定义一个新的接口(对要实现的功能加以抽象),和一个实现该接口的Adapter类来透明的调用外部组件。保证外部组件替换时,我们只替换adapter类

单元测试

  • TDD三定律:先写case,再写生产代码
    • 在编写能通过的单元测试前,不可编写生产代码
    • 只可编写刚好无法通过的单元测试,不能编译也算不通过
    • 只可编写刚好足以通过当前失败测试的生产代码
  • 整洁的测试
    • 保持测试代码的整洁性,测试代码必须随生产代码的演进而修改
    • 保持测试代码的可读性
    • 每个单元测试函数中应该有且只有一个断言语句
    • 尽可能禁烧每个概念的断言数量,每个测试函数只测试一个概念
  • FIRST
    • 快速(Fast):运行足够快
    • 独立(Independent):测试case之间不能相互依赖
    • 可重复(Repeatable):测试case在任意环境中可以重复通过
    • 自足验证(Self-Validating):测试结果依靠bool值输出,不要人肉自己看结果。
    • 及时(Timely):测试应及时编写,测试只比生产代码早写几秒钟。

  • 类的结构
    • 从上倒下顺序为:公共静态常量 ——> 私有静态常量 ——> 实体变量 ——> 公共变量 ——> 公共函数 ——> 被公共函数调用的私有工具函数
  • 类的原则
    • 类应该尽可能的短小,并非代码量短小而是权责小。
    • 类的名称应该描述权责,当无法为类命名精确的名称时,大概率是权责大了
    • 单一权责原则(srp),类或者模块应有且只有一条加以修改的理由,每个模块或者类只做一件事情,且不由多个团队共同维护
    • 系统应该有诸多小类组成,而不是几个大类
    • 内聚,类中应该有少量的实体变量,尽量让类中的每个变量都被每个方法使用。如果类中的某个变量没有被任何方法使用,应该考虑拆分该变量
    • 保持函数,尤其是参数个数短小的策略,可能会导致一组方法所用的实体变量数增加,可以考虑将这些变量和方法拆分到两个或多个类中,增强新类的内聚性
    • 开放-闭合原则(OCP):对拓展开放,对修改封闭。添加新特性不应对原有代码进行修改
    • 依赖倒置原则:类应该依赖于抽象而不是依赖于细节。

系统

恰当的抽象等级和模块,可以让个人和其所管理的组件即便不了解全局也能有效运转

  • 系统的构造和使用
    • 将全部构造内容都放到main中,其余部分单独设计,且设计时假设所有对象都已正确构造和设置。
    • 通过工厂模式将构造过程封装
    • 通过依赖注入和控制反转对不同类进行解藕
    • 不在一开始做大的、过度的架构设计,而是不断演进迭代
    • 恰当的切分关注面,这点没太看懂

迭代

通过不断的实现小而正确的迭代完成大项目

  1. 运行所有测试
    • 系统必须如预期般工作
    • 不可验证的系统,绝不应该部署上线
    • 紧耦合的代码难以编写测试,可测试会导向类短小且目的单一的设计
  2. 重构
    • 有了测试能保持代码和类的整洁,方法就是递增式的重构代码
    • 测试消除清理代码会破坏代码的恐惧
    • 三条规则:消除重复、保证表达力、减少类和方法的数量
  3. 不可重复
    • 不但雷同的代码是重复,在功能上也要消除重复
    • 进行共性抽取时违反SRP原则,可以将新方法放到另外的类中,实现可复用
    • 模版方式模式(包括继承和模版)是移除高层级重复的通用技巧
  4. 具备良好的表达力
    • 软件项目的主要成本是长期维护
    • 使用好名称
    • 保持函数和类的短小,通过这种更易命名
    • 命名采用标准命名法
    • 测试用例也应具有表达性,通过测试用例起到文档的作用

并发编程

  • 为什么要并发
    • 并发是一种解藕,把做什么(目的)和何时做(时机)分解开
    • 单线程目的和时机紧耦合,解藕能明显改进吞吐量和结构
    • 单线程在等待socket的io上花费太多时间,改成同时访问多站点的多线程算法,可改进性能
  • 并发中肯说法
    • 只有在多个线程或处理器能分享大量等待时间时有用
    • 目的和时机的解藕对系统结构产生巨大影响
    • 并发在性能和编写额外代码上增加开销
    • 正确的并发是复杂的
    • 并发的缺陷或者问题并非总能复现,常被看做偶发事件而忽略
    • 并发需要对设计策略做根本性修改
  • 并发防御原则:
    • 单一权责srp:分离并发相关代码和其他代码
    • 限制数据作用域(数据封装,严格限制对可能被共享数据的访问)
    • 使用数据副本
    • 线程应尽可能的独立。(处理的数据源头也是独立的)
  • 并发基础概念
    • 互斥:同一时刻只有一个线程能访问共享资源和数据
    • 线程饥饿:一个或者一组线程长时间等待,始终由运行快的获取cpu资源,导致运行慢的永远没有机会执行
    • 死锁:两个或多个正在运行的线程相互持有对方结束的资源。导致都无法结束
    • 活锁:任务或资源没有被阻塞,但是资源竞争激烈,一直处于尝试、失败的过程。
  • 并发的三种线程模型:
    • 生产者消费者(互斥,限定资源)
    • 读者-作者(类似多读多写共享资源,线程饥饿、吞吐量降低)
    • 哲学家就餐模型(死锁、活锁、吞吐量和效率)
  • 临界区应该被锁起来,尽可能少的设计临界区
  • 线程代码的测试
    • 编写有潜力暴露问题的测试,在不同配置、负载下频繁运行,有失败的测试用例要跟踪,不要忽略
    • 线程代码导致了看似不可能的失败,不要归结于偶发事件,也不要忽略
    • 先保证单一线程可运行,不要单一线程和多线程同时测试
    • 编写可拔插的代码,线程配置不要写死,要根据配置和吞吐量修改
    • 运行多于处理器数量的线程
    • 在不同平台运行
    • 通过硬编码的方式设置线程等待,查看线程是否正常工作
    • 主动搞一些试错代码(比如随机休眠,随机让步),测试线程代码的稳定性