整洁代码的目的
是为了使代码更具可读性和可拓展性
总体有三个基调
- 随着系统的维护,代码可以进行不断的优化
- 优化的方式有多种,并没有固定正确的一种
- 当整洁代码的理念和强制性的标准冲突时,遵循强制性标准
总体目录
具体内容
命名
好的命名
- 常量和变量:使用名词和形容类的短语,如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值
- 尽量不要使用传出参数。使用改变的对象状态或者返回值来代替穿出参数
- 不要重复自己
- 有重复的代码就提炼出一个函数来
- 使用异常而不是错误码
- 错误处理只做一件事情,不要多做。
注释
总体思想
- 软件中最有用的是代码而非注释
- 注释不能美化代码,与其多花时间写注释,不如多花时间整理代码
- 好注释
- 法律信息
- warning和todo
- 代码逻辑不合理的解释(更建议去整理代码)
- 开放api的解释
- 坏注释
- 自言自语
- 多余的注释:代码能表达清楚就不要注释了
- 误导性的注释:更新代码记得更新注释
- 日志性的注释:当成日志来写的注释
- 注释掉的代码:应该直接删除掉
格式
- 水平格式:
- 使用缩紧,大部分ide会自动缩进
- 使用空格,使代码更具可读性
- 该换行就换行
- 垂直格式:
- 高层次的概念和函数放到文件的顶部,低层次的细节和函数放到下面
- 不同概念要用隔开,相近的要靠近。减少读者的翻页频率
- 垂直距离
- 变量声明的地方要靠近使用的地方
- 相关函数、相关概念放在一起
- 被调用者放到调用者的下面
- 团队规则
- 一个团队一套代码规则
真实对象和数据容器
- 真实对象:要尽可能的隐藏细节,调用方不关心内部细节,只调用暴露的公共api
- 数据容器:内部的数据都是公开的,不抽象公共的api,只用来存储数据或者传参数
- 对于真实对象的类,在不改动现有struct的前提下,可以直接增加新的函数
- 对于数据容器的类,通过多态,增加新的类
- 如果要将多个属性封装成类,那么实例化时应该有实例可以做的事情,而不仅仅只是返回类内部的数据结构
- 不要火车式的调用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中,其余部分单独设计,且设计时假设所有对象都已正确构造和设置。
- 通过工厂模式将构造过程封装
- 通过依赖注入和控制反转对不同类进行解藕
- 不在一开始做大的、过度的架构设计,而是不断演进迭代
- 恰当的切分关注面,这点没太看懂
迭代
通过不断的实现小而正确的迭代完成大项目
- 运行所有测试
- 系统必须如预期般工作
- 不可验证的系统,绝不应该部署上线
- 紧耦合的代码难以编写测试,可测试会导向类短小且目的单一的设计
- 重构
- 有了测试能保持代码和类的整洁,方法就是递增式的重构代码
- 测试消除清理代码会破坏代码的恐惧
- 三条规则:消除重复、保证表达力、减少类和方法的数量
- 不可重复
- 不但雷同的代码是重复,在功能上也要消除重复
- 进行共性抽取时违反SRP原则,可以将新方法放到另外的类中,实现可复用
- 模版方式模式(包括继承和模版)是移除高层级重复的通用技巧
- 具备良好的表达力
- 软件项目的主要成本是长期维护
- 使用好名称
- 保持函数和类的短小,通过这种更易命名
- 命名采用标准命名法
- 测试用例也应具有表达性,通过测试用例起到文档的作用
并发编程
- 为什么要并发
- 并发是一种解藕,把做什么(目的)和何时做(时机)分解开
- 单线程目的和时机紧耦合,解藕能明显改进吞吐量和结构
- 单线程在等待socket的io上花费太多时间,改成同时访问多站点的多线程算法,可改进性能
- 并发中肯说法
- 只有在多个线程或处理器能分享大量等待时间时有用
- 目的和时机的解藕对系统结构产生巨大影响
- 并发在性能和编写额外代码上增加开销
- 正确的并发是复杂的
- 并发的缺陷或者问题并非总能复现,常被看做偶发事件而忽略
- 并发需要对设计策略做根本性修改
- 并发防御原则:
- 单一权责srp:分离并发相关代码和其他代码
- 限制数据作用域(数据封装,严格限制对可能被共享数据的访问)
- 使用数据副本
- 线程应尽可能的独立。(处理的数据源头也是独立的)
- 并发基础概念
- 互斥:同一时刻只有一个线程能访问共享资源和数据
- 线程饥饿:一个或者一组线程长时间等待,始终由运行快的获取cpu资源,导致运行慢的永远没有机会执行
- 死锁:两个或多个正在运行的线程相互持有对方结束的资源。导致都无法结束
- 活锁:任务或资源没有被阻塞,但是资源竞争激烈,一直处于尝试、失败的过程。
- 并发的三种线程模型:
- 生产者消费者(互斥,限定资源)
- 读者-作者(类似多读多写共享资源,线程饥饿、吞吐量降低)
- 哲学家就餐模型(死锁、活锁、吞吐量和效率)
- 临界区应该被锁起来,尽可能少的设计临界区
- 线程代码的测试
- 编写有潜力暴露问题的测试,在不同配置、负载下频繁运行,有失败的测试用例要跟踪,不要忽略
- 线程代码导致了看似不可能的失败,不要归结于偶发事件,也不要忽略
- 先保证单一线程可运行,不要单一线程和多线程同时测试
- 编写可拔插的代码,线程配置不要写死,要根据配置和吞吐量修改
- 运行多于处理器数量的线程
- 在不同平台运行
- 通过硬编码的方式设置线程等待,查看线程是否正常工作
- 主动搞一些试错代码(比如随机休眠,随机让步),测试线程代码的稳定性