整洁代码
代码是需求的精确性表达 代码不会消失
- 读起来令人愉悦
- 只做好一件事
- 明确地展现出要解决问题的张力
- 整洁代码以测试作为基础
- 不要重复代码
- 深合己意
代码更多的时候是用来读
命名
名副其实:变量 函数 类的名称要充分体现它们的作用
避免误导:
- 避免使用与本意相悖的词
Accounts accountList; // bad
List<Account> accountList; // good
- 谨慎使用不同之处特别小的名称
var userPermissionControllService;
var userPermissionControllerService;
- 以及小写字母l与大写字母O与数字 1 0 相似的情况
int l = 0;
int o = 1;
if (l == 0) return o = l;
有意义的区分:
- 避免使用数字系列命名
void copy(StringBuffer s1, StringBuffer s2); // bad
void concat(StringBuffer source, StringBuffer target); // good
- 避免使用意义相同的名称
class Product{}
class ProductInfo{} // 加个Info并没有说明什么
class ProductDetail{}
使用读得出来的名称: 方便讨论
使用可搜索的名称:
- 为常量命名 方便维护
double circleArea = 3.14 * Math.pow(radius, 2); // bad
double CIRCLE_PI = 3.1415926; // good
- 名称长短与其作用域大小相对应
private static final double CIRCLE_PI = 3.14;
void calcArea() {
final double PI = 3.14
...
}
避免使用编码:这些技巧在IDE智能的时代有它的用处
- 避免匈牙利标记法在变量名称携带类型
int iPort = 8080; // 该变量为int类型 bad
- 避免前缀标记成员变量
private List<Listeners> m_listeners; // bad
- 避免避免接口与实现携带I前缀或者Imp后缀
interface IUserService{} // bad
class UserServiceImp implements UserService {} // bad
class DefaultUserService implements UserService {} // good
避免思维映射:传统管用i j 表示循环计数器 其他情况下要避免使用单字母
for (int i=0;i<MAX;i++){...} // 遵循传统惯例
int r = calcArea(); // bad
类名与对象名应该是名词或者名词短语
class Customer{}
Processor processor;
方法名应该是动词或者动词短语
void getServerInfo();
命名时避免抖机灵
threadPoll.kill(); // bad
threadPoll.shutdown(); // good
使用概念一致的命名:
- SELECT DELETE UPDATE INSERT
避免将同一术语用于不同概念
// bad
void addUser();
BigDecimal addPrice(BigDecimal target);
尽量使用技术性名称而非业务领域名称 是在没有技术名词 与问题领域更近的代码 可以采用业务领域的名称
Queue<Job> jobQueue; // 技术名词
DinnerOrder order; // 业务名词
如果无法通过类或者方法来给名称提供上下文 那么只能给名称添加前缀来添加上下文了
class Address {
String username;
String phone;
String country;
String postCode;
}
String addressCode; // 在一个没有上下文的环境中
短名称够清楚就行了 不要添加不必要的上下文
class ShopSystemUserService {} // bad
函数
短小:
- 块内调用的子函数具有说明性
String renderJsp(){
var classCode = compileJsp();
return executeJspService(classCode);
}
- 不该有复杂的嵌套结构
void badFunction() { // bad
if (..) {
while(){
...
for(..){..}
}
}
}
只做一件事:函数内部的实现充分体现函数名称
确保函数中的语句在同一抽象层级上面
String renderJsp(){
var classCode = compileJsp();
return executeJspService(classCode);
}
使用多态取代switch语句
// bad
Money calcPay(Employee e){
switch(e.type) {
case MANAGER:
return e.getPay() - 20%;
case COMMON:
return e.getPay() - 10%;
...
}
}
// good
abstract class Employee{
abstract Money getPay();
}
class CommonEmployee{
Money getPay(){...}
}
class ManagerEmployee{
Money getPay(){...}
}
使用描述性的名称能理性设计思路 帮助改进之
var result;
var searchResult;
var movieSearchResult; // best
函数参数:
- 参数越多函数越难理解
public void convertAndSend(Object object){..}
public void correlationConvertAndSend(Object object, CorrelationData correlationData){..}
public void convertAndSend(String routingKey, final Object object, CorrelationData correlationData){...} // bad
exchange.send(String rotingKey,Object msg); // better
- 使用标志参数(boolean)就代表函数不止做一件事 应该拆分成两个函数
void submitTask(Task t, boolean flag){ // 尤其flag命名并不能说明做什么 改成isSync 可能好一点
if (flag) {
sync
}else {
async
}
}
// good
void submitTaskAsync(){...}
void submitTaskSync(){...}
- 函数和参数应当形成一种动词/名词对形式
write(PrintWriter pw, String msg); // bad
printWriter.write(msg); // good
副作用:避免使用输出参数(out) 需要修改状态 就将该状态作为对象的属性
void removeNegative(List<Integer> list); // bad
list.removeIf(...); // good
分割指令与询问:函数要么做什么 要么回答什么 不能两者得兼
boolean set(String k, String v){ // bad 这个函数承担了两个职责
if (exists){
return true;
}
...
return false;
}
// good
boolean exists(String k);
void set(String k,String v);
异常代替错误码:
- 错误处理代码就能从主路径代码分离出来
// bad
if (!err){
if (!err){
...
}
}
// good
try {
} catch (Error1){
} catch (Error2){
}
- 主体以及错误处理代码可以抽离成函数
try {
generateSearchResult();
} catch(){
logError();
sendErrorMsg();
}
- 错误码枚举一旦发生修改 依赖其的模块都要重新编译 使用继承异常的方式可以进行平滑扩展
别重复自己:重复可能是软件中一切邪恶的根源
结构化编程:单一出入口原则在大函数中才有明显的好处
注释
- 注释容易与代码不一致 欺骗读者
- 注释无法美化代码 糟糕的代码还是糟糕的代码
- 尽可能使用代码阐述你的意图 而非注释
好的注释
- 法律信息
- 提供信息
interface SessionFactory {
// 新建一个数据库连接并返回
Session openSession();
}
- 对意图的解释
// 寻找0到n的素数 根据数学证明 只要到n的平方根就行了
for(int i=0;i<Math.sqrt(n);i++){...}
- 阐释一些难以理解的参数或者返回值
// 发送对象为空,代表是一条广播消息
if (StringUtils.isEmpty(payload.getTo())){
...
} else {
broadcast
}
- 警示会出现某种后果
// 该方法使用一个listener的确认 使用synchronized关键字保证只有一个线程能进入
public synchronized ConfirmResult sendTextMessage(String target, String text) {...}
- TODO注释
// 向消息队列写入消息:订单 订单详情 TODO
- 强调方法貌似不合理之处的重要性
void onMessage(ByteBuf buf){
...
buf.release(); // 需要减少缓冲区的引用计数
}
- 公共 API 中的 Javadoc
坏注释
- 无法给读者提供有效的信息
// 提交任务
boolean success = submitTask();
- 多余的注释/废话注释
编写代码时 着重于代码的表现力 而非加之以注释
// bad 等待timeout个时间 然后关闭
void close(int timeout){
wait(timeout);
close();
}
- 误导性注释 代码与注释所说的不是一回事
- 循规蹈矩注释:每个方法变量都要javadoc
- 日志式注释记录每一次修改 在版本控制系统出现后意义不大
- 标记栏注释
// 注意!!! ////////////
特别重要才使用 使用多的话 就会被淹没在大量斜杠中
- 括号后面的注释
对于大函数或许才有意义
try{
if (){
while(){
}//while
} // if
}catch{
} // catch
- 作者与署名 同样 VCS可以工作的更好
- 注释掉的代码
- 包含着HTML标签的注释
- 携带非本地信息
// 提交任务 每隔5分钟运行一次 这里的5分钟跟这个函数毫无关系
void submitTask();
- 信息过多 将一些RFC提案整个添加到注释里
格式
原始代码其代码风格和可读性仍会影响到其可维护性和可扩展性
垂直格式
短文件比常文静更易于理解
// 紧密在一起的代码代表概念相关
DeliveryInfoDO deliveryInfoDO = new DeliveryInfoDO();
deliveryInfoDO.setBuilding(deliveryDTO.getBuilding());
deliveryInfoDO.setDetail(deliveryDTO.getDetail());
// 使用空白行隔开 每个空白行都是一个线索
deliveryRepository.save(deliveryInfoDO);
if (deliveryDTO.getDefaultDelivery() != null && deliveryDTgetDefaultDelivery()) {
consumerDeliveryRepository.resetDefaultDelivery(consumer.getUserId());
}
垂直距离:
关系密切的概念应相互靠近
- 本地变量声明尽可能靠近其使用位置
- 实体变量声明在类的顶部(Java)
- 有联系的函数放在一起 调用者尽可能在被调用者上面
横向格式
尽力保持代码行短小
使用空格分割相关性较弱的元素:
- 分割赋值操作符
int[] data = new int[10];
- 分割函数参数
deliveryService.updateDelivery(token, deliveryId, deliveryDTO);
使用缩进表现源文件的继承结构 缩进可以快速展现出当前的范围
对象和数据结构
过程式代码容易在不改动数据结构的情况下增加函数
面向对象则容易在不改动函数的情况下增加新类
迪米特法则:模块不应了解它所操作对象的内部情形
// bad
String url = host.getContext().getServlet().getName();
避免在DTO中塞入逻辑 保持简单setget即可
错误处理
- 使用异常而非返回码
- 先写try-catch语句
try-catch定义了一个范围 使用TDD开发剩下的逻辑
- 使用不可控异常
可控异常违反了开闭原则 底层的修改会直接贯穿到高层
- 构造异常时 提供足够的环境说明 以便快速排错
- 根据调用者需要定义异常 也就说打包第三方 API
try {
} catch(ThirdPartException e){
throw new BusinessException(e);
}
- 使用特例模式来避免应付异常
// bad
try {
getEmployee().run();
} catch(NullPointerException e){
...
}
void getEmployee(){
maybe return null;
}
// good
void getEmployee(){
normal return new Employee();
sometimes return new EmptyEmployee();
}
- 避免传递null
边界
整洁的边界应该避免我们的代码过多了解第三方代码中的信息
第三方包
封装第三方API来避免在系统中传递使用第三方接口
学习性测试:通过编写测试来学习第三方API
- 不仅可以学习API使用
- 同时测试也是更新第三方包时的保障
使用尚不存在的代码
通过适配器适配尚未实现的接口 来进行已知与未知的隔离
// 未知
interface ThirdPartInterface{...}
// 未知与已知的交界处
interface ThirdPartAdapter extends ThirdPartInterface{...}
类
组织:
public static int PORT = 8080;
private static String MAGIC_NUMBER = 0XCAFE_BABE;
private String instanceName;
protected String subName;
public void run(){...}
private void innerRun(){...}
尽可能进行封装 除非玩不得以 否则不要暴露
类应该短小:
判断类短小的标准使用职责数来衡量 而非代码行数
系统应该由许多短小的类而非少量巨大的类组成
类应只由少量实体变量组成 这些变量如果同时被越多的函数操作 就代表这个类内聚性越高
通过拆分函数以及函数相关的实体变量到其他类来将一个大类拆分为几个小类
方便修改的组织:
- 符合OCP
- 使用接口隔离修改
当采取诸如DIP等原则时 系统各个组件的耦合就已经非常低了 此时也方便测试
系统
分离系统的构造与使用
使用main组件:
使用工厂控制对象的创建
使用依赖注入容器来管理对象
测试驱动系统架构
代码层面与架构关注面分离开 避免侵入性代码
没有必要先做大设计
延迟决策
使用DSL
填平了领域与实现之间代码的壕沟
迭进
简单设计原则:
- 运行所有测试 会促使类短小且单一 符合SRP 同时测试越多 代码之间耦合越低 符合DIP
- 重构 测试消除了对修改代码的恐惧
- 不可重复
- 代码具有良好的表达力 使用好名称 标准命名法 良好的单元测试也能表达出某个类的作用
- 尽可能减少类和方法 似乎违反SRP 但是这条规则与SRP达到一个平衡
并发编程
并发解耦了目的与时机
一些问题:
- 并发一般只有在IO密集型的程序或者有多个处理器上的机器才有效率提升
- 并发系统的设计与单线程设计极不相同
- 正确的并发是很复杂的
防御原则
谨记数据封装 限制数据作用域 严格限制多个线程访问的共享数据
使用数据复本 有些情况下的并发可以只读 这个时候可以使用复制的方式避免共享
线程应尽可能独立 不与其他线程共享数据
执行模型
大部分并发问题都是下列模型的变种:
- 生产者-消费者 限定资源模型 有着固定数量的资源
- 读者-写者 读写问题
- 哲学家就餐问题 资源竞争问题
建议
不要在客户端调用一个对象的多个同步方法: 这可能造成多个线程下的数据不一致问题
解决:
- 客户端代码锁定
synchronized(lock){
obj.f1();
obj.f1();
}
- 服务端锁定
// AtomicInteger
public int incrementAndGet(){}
- 适配服务端 使用适配器模式
保持同步区域微小
尽早考虑关闭问题
测试
将偶然的失败看做线程问题
先确保单线程代码可工作
配置多线程代码在不同的配置环境下执行
在代码里插入试错代码:sleep yeild
- 手工
- aop
速查
注释
- 注释应只包含有关代码的技术信息 像修改时间 作者等没必要放入注释
- 对于过时 不正确的注释 这些注释很快就会消失 少写
- 少写废话注释 代码已经能表达的 没必要加注释
- 注释需要花时间写到最好
- 代码不要注释 直接删除
环境
系统构建与运行单元测试应只需一个指令
函数
- 参数越少越好
- 输出参数违反直觉 避免使用
- 标识参数应该被消灭
- 丢弃永不调用的方法
一般性问题
- 理想的源文件只包含一种编程语言
- 函数或者类的实现应该是其他程序员所期待的
- 对于测试需要追索每种边界条件
- 忽视安全警告相当危险
- 重复
- 最明显的是重复代码
- 较隐蔽的是相同的条件判断 if switch链条
- 最隐蔽的是算法相似 但代码不同
- 建立合适的抽象层级
- 抽象类来容纳较高层的概念 实现类容纳较低层
- 通常来说 父类对之类一无所知
- 隐藏数据 通过隐藏来达到限制信息 从而控制耦合度
- 删除不执行的死代码
- 垂直分隔
- 本地变量应该定义在其首次使用的上面 私有函数应该定义在其首次使用的下面
- 概念前后不一致
- 毫无关系的东西不该耦合
- 类的方法应该只对它自己的变量及函数感兴趣 少去依赖外部类的变量函数
- 代码要充分展现作者意图
- 对于需要考虑动态行为的静态方法 可能有问题
- 使用临时变量存储计算过程 提供可读性
- 函数名称应表达其行为
- 要理解自己编写的算法
- 模块的依赖应该是物理依赖
- 使用多态取代if switch
- 遵循团队代码规范
- 用命名常量代替魔数
- 避免代码含糊不清
- 良好的结构优于良好的命名
- 对条件表达式进行命名
- 一个函数只做一件事
- 暴露函数调用的前后顺序
- 封装边界条件
- 函数应该只有一种抽象层级
- 配置数据放在高层
- 避免传递浏览 也就是最小知道原则
Java
- 使用导入通配符避免过长的导入列表
- 不要通过继承的方式使用常量
名称
- 使用描述性的名称
- 名称应与抽象层级相符
- 使用标准命名法
- 名称无歧义
- 为较大的范围选用较长的名称
- 避免编码 前缀后缀
- 名称要说明副作用
测试
- 使用覆盖率测试覆盖大部分测试
- 注意边界条件
- 缺陷可能扎堆发生
- 测试要快