JAVA代码整洁之道
整洁代码的定义(优雅而高效)
1. 代码逻辑直接了当,让问题难以隐藏
2. 尽量减少依赖关系,以便于维护
3. 确定并统一分层策略,按照规定放置代码, 建议先分层目录,再建立业务文件夹,然后按照分层归属放置文件
4. 将性能调整到最优,让别人不会来优化代码
破窗效应
环境中的不良现象如果被放任存在,会诱使人们仿效,甚至变本加厉。
如果代码写的很糟糕,别人在仿照的时候就会更加糟糕。所以代码质量管控需严格且不留意外。
越是复杂逻辑的代码,越需要代码整洁。例如内存泄漏,静态条件代码,这些高并发的场景,更需要严格执行。
优化的思路
1. 应有单元测试和验收测试。
不推荐使用脏测试,随便命名等,因为随着接口后面修改单元测试的代码也需要跟着一起改。
单元测试的结构应为:构造 (构造测试数据)- 操作(执行要测试方法) - 检验(校验测试数据准确性),其中测试数据准确性时应用代码里写Assert判断,而不是通过人工或者日志比对,JUnit中每个测试函数都应该有且只有⼀个断⾔ 语句。
可以使用模板方法模式优化重复代码
一个测试函数只测试一个功能
测试 F.I.R.S.T原则,快速、独立、可重复、自足验证、及时
2. 使用有意义的命名
不要怕命名太长,要一看就能知道他是做什么的, 更便于搜索。
类名用名词修饰,方法名用动词命名,
同时要遵从分层的尾椎(Service,Controller)。
如果用到设计模式要把设计模式的关键词命名到类名里(Factory,Adapter),要尽量体现系统种的设计理念。 IShapeFactory 和 ShapeFactory,可以直接命名为ShapeFactory不用带个I。
同时更要避免错误的命名,误导其他编程人员理解。
拒绝使用单个字母 i,I,1,o,O,0 命名变量,避免单字母命名。
前后不同代码里的相同意义的变量名要尽量保持一致,避免将同一单词用于不同目的
避免带数字的命名方式,如:a1,aN
给命名的类和方法名添加前缀,更能表明他的意义。如AdressXXX
3. 做一件事只提供一种途径, 一个方法只做一件事,
且方法最大不可以超过200行,拆成一个个小的方法,可以先写完,再进行抽取重构。
宽度不要超过120字符,idea有插件能看到对应长度。
方法应该是短小的,一个类里的代码都是自己相关的方法。要检查对象或方法是否想做的事太多。如果一个对象的功能太多,最好是切分成俩个或者多个对象,或者采用抽象的手段重构他
4. 尽量少的依赖关系
5. 提供明确清晰且尽量少的api
6. 代码含义清晰,代码应该在字面上表达其含义。
理清思路后再进行编码,避免条理不清晰或跳跃性的代码
7. 尽量减少重复的代码,抽象,公共提取工具类。拆散类,避免单个类中的内容太多
8. 方法的命名规则要前后保持一致,如果用GetXX就全用这种来命名
9. if、else、while语句,里面的代码块应该只有一行代码,应该是一个函数调用的语句,对应的判断里的逻辑也要尽量用方法封装这样阅读起来更具有意义。例如:if (employee.age > 65) 不如改成 if (employee.isOldPerson())
10. 函数只干一件事,判断函数是否只做了一件事,可以通过看这个函数是不是还能继续拆分成其他的函数
11. 函数的放置位置,应该按照调用关系,从上到下,相互调用的函数应该紧挨着,调用的一方放在上面。
类应该从⼀组变量列表开始。如果有公共 静态常量,应该先出现。然后是私有静态变量,以及私有实体变量。 很少会有公共变量。 公共函数应跟在变量列表之后。
我们喜欢把由某个公共函数调⽤ 的私有⼯具函数紧随在该公共函数后⾯。这符合了⾃顶向下原则。
构造出的类应该尽量短小,同时对外暴露的方法应尽量少,一个类只做一件事,单一全责类,
系统应该由许多短⼩的类⽽不是少量巨⼤的类组 成。每个⼩类封装⼀个权责
12. 参数个数最多不要超过3个,参数越少越是理想。如果一定要超过3个参数可以封装成参照对象
13. 尽量减少出参,如要修改什么,可以在入参中对象修改
14. 不要在方法里传Boolean类型参数,如果需要标识判断逻辑,可以将函数拆成 renderTrue();和 renderFalse();
15. 如果方法传参有多个且传参类型无法分辨出对应的意义,建议在方法名命名的时候按参数顺序的命名。如:writeDataByPathAndFileName(String path,String fileName)
16. 使用异常代替返回错误码,直接throw出异常信息。使⽤异常替代错误码,新异常就可以从异常类派⽣出来,⽆需重 新编译或重新部署。
异常抛出要抛出具体异常。而不是Exception包含,
推荐使用自定义异常,方便后面扩展可以直接继承出新的异常
17. Try/Catch语句里应该只放函数调用的语句,而不是具体代码逻辑。且catch/finally后面不应该跟其他代码内容
18. 不准确的注释比没注释更要命,
尽量减少注释,避免后面维护修改代码,但是没维护注释的情况。
注释这个本就是代码无法被人看懂的无奈之举,真正优秀整洁的代码一眼就可以看出来是干嘛的。
不需要的代码直接删掉,而不是注释掉,现在都有git工具,不要害怕删代码。
更不要把自己的喃喃自语放到注释里。
不用把日志性的注释,如谁谁谁在啥时候改了啥,都有git工具看,不需要写在代码里。
不要把注释放到括号后面 。 如 if (xxx()) // 判断xxx
19. 要保留警告性注释,比如该段代码注意事项。和TODO注释,并定期检查TODO的完成程度及时删掉。
公共Api的注释要写的详细,参考Javadoc,除了公共接口应该标清楚注释信息,其他的方法等应尽量减少无用的注释,避免注释掉之前的代码,避免参数的意义描述,应用命名规范让直接可读
20. 在封包声明、导⼊声明和每个函数之间,都 有空⽩⾏隔开。这条极其简单的规则极⼤地影响到代码的视觉外观。 每个空⽩⾏都是⼀条线索,标识出新的独⽴概念。
相关联的代码应该紧挨着,实体变量应该在类的顶部声明
21. 团队应在编码开始前就统一编码风格,可以开10分钟会讨论在什么地⽅放置括号, 缩进⼏个字符,如何命名类、变量和⽅法。
也可以直接共用一套固定的Idea的编码格式化模板
22. 数据结构方面,更偏向于抽象或接口。不愿意暴露数据细节,更愿意使用抽象形态表示数据。对象暴露行为,而隐藏数据。
23. 方法不可以返回null值,如List 如果返回为空就返回Collections.emptyList()。
如果你在调⽤某个第三⽅API中可能返回null值的⽅法,可以考虑 ⽤新⽅法打包这个⽅法,在新⽅法中抛出异常或返回特例对象
24. 方法入参不要接收null值,可以使用入参校验,如果必要参数为空,抛出异常,或者使用断言
25. 对接第三方API时,如对方尚未完成,可以使用先完成自己的代码,定义好interface后。使用Adapter进行跨接
26. 代码在设计的时候,应该提升内 聚性,降低耦合度,切分关注⾯,模块化系统性关注⾯,缩⼩函数和 类的尺⼨,选⽤更好的名称。消除重复,保证表达⼒,尽可能减少类和⽅法的数量
27. 对象是过程的抽象。线程是调度的抽象。
多线程应该限制数据的作用域。或可以使用数据的复制体,从而避免使用共享数据。
线程应尽可能独立,而不是子线程混杂。
使用生产者消费者模型来避免多线程。
,要保证对应锁的方法代码块尽量小,避免资源争⽤、降低执⾏效率
整理思路遵从的设计原则
1. 单一权责原则
2. 开闭原则
3. 依赖倒置原则
案例1
优化前
public List<int[]> getFlaggedCells() {
List<int[]> flaggedCells = new ArrayList<int[]>();
for (int[] cell : gameBoard)
if (cell[STATUS_VALUE] == FLAGGED)
flaggedCells.add(cell);
return flaggedCells;
}
优化后
public List<Cell> getFlaggedCells() {
List<Cell> flaggedCells = new ArrayList<Cell>();
for (Cell cell : gameBoard)
// 将枚举或者常量的对比封装成一个方法更容易理解含义
if (cell.isFlagged())
flaggedCells.add(cell);
return flaggedCells;
}
案例2 - 静态工厂方法命名
优化前
Complex fulcrumPoint = new Complex(23.0);
优化后
// 写成方法,会更有意义,可以考虑将构造器设置成private, 强制使用静态方法命名
Complex fulcrumPoint = Complex.fromRealNumber(23.0);
案例3 - 命名改造
优化前
private void printGuessStatistics(char candidate,
int count) {
String number;
String verb;
String pluralModifier;
if (count == 0) {number = "no";
verb = "are";
pluralModifier = "s";
} else if (count == 1) {
number = "1";
verb = "is";
pluralModifier = "";
} else {
number = Integer.toString(count);
verb = "are";
pluralModifier = "s";
}
String guessMessage = String.format(
"There %s %s %s%s",
verb,
number,
candidate,
pluralModifier
);
print(guessMessage);
}
优化后
public class GuessStatisticsMessage {
// 将方法里的传参改成类的属性,再通过其他方式赋值,同时拆分函数
private String number;
private String verb;
private String pluralModifier;
public String make(char candidate, int count) {
createPluralDependentMessageParts(count);
return String.format(
"There %s %s %s%s",
verb, number, candidate, pluralModifier );
}
private void createPluralDependentMessageParts(int count) {
if (count == 0) {
thereAreNoLetters();
} else if (count == 1) {
thereIsOneLetter();
} else {
thereAreManyLetters(count);
}
}
private void thereAreManyLetters(int count) {
number = Integer.toString(count);
verb = "are";
pluralModifier = "s";
}
private void thereIsOneLetter() {
number = "1";
verb = "is";
pluralModifier = "";
}
private void thereAreNoLetters() {
number = "no";
verb = "are";
pluralModifier = "s";
}
}
案例4 - switch语句优化
sitch既占用大量的代码块,又违反了单一权证原则和违反开放闭合原则,没回添加新类型都需要修改。
可以使用多态,或者工厂来优化他
优化前
public Money calculatePay(Employee e)
throws InvalidEmployeeType {
switch (e.type) {
case COMMISSIONED:
return calculateCommissionedPay(e);
case HOURLY:
return calculateHourlyPay(e);
case SALARIED:
return calculateSalariedPay(e);
default:
throw new InvalidEmployeeType(e.type);
}
}
优化后
public abstract class Employee {
public abstract boolean isPayday();
public abstract Money calculatePay();
public abstract void deliverPay(Money pay);
}
-----------------
public interface EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r)
throws InvalidEmployeeType;
}
-----------------
public class EmployeeFactoryImpl implements
EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r)
throws InvalidEmployeeType {
switch (r.type) {
case COMMISSIONED:
return new CommissionedEmployee(r);
case HOURLY:
return new HourlyEmployee(r);
case SALARIED:
return new SalariedEmploye(r);
default:
throw new InvalidEmployeeType(r.type);
}
}
}
案例5 - 数据结构抽象出来
对象把数据隐藏
于抽象之后,曝露操作数据的函数。数据结构曝露其数据,没有提供
有意义的函数
得墨忒⽿律认为,类C的⽅法f只应该调⽤以下对象
的⽅法:
C 由
f创建的对象;
作为参数传递给f的对象;
由C的实体变量持有的对象。
⽅法不应调⽤由任何函数返回的对象的⽅法
最为精练的数据结构,是⼀个只有公共变量、没有函数的类。这
种数据结构有时被称为数据传送对象,或DTO
优化前
public class Square {
public Point topLeft;
public double side;
}
public class Rectangle {
public Point topLeft;
public double height;
public double width;
}
public class Circle {
public Point center;
public double radius;
}
public class Geometry {
public final double PI = 3.141592653589793;
public double area(Object shape) throws
NoSuchShapeException {
if (shape instanceof Square) {
Square s = (Square) shape;
return s.side * s.side;
} else if (shape instanceof Rectangle) {
Rectangle r = (Rectangle) shape;
return r.height * r.width;
} else if (shape instanceof Circle) {
Circle c = (Circle) shape;
return PI * c.radius * c.radius;
}
throw new NoSuchShapeException();
}
}
优化后
// 合理使用private封装数据。对外暴露更多的方法,而不是数据
public class Square implements Shape {
private Point topLeft;
private double side;
public double area() {
return side * side;
}
}
public class Rectangle implements Shape {
private Point topLeft;
private double height;
private double width;
public double area() {
return height * width;
}
}
public class Circle implements Shape {
private Point center;
private double radius;
public final double PI = 3.141592653589793;
public double area() {
return PI * radius * radius;
}
}
优化前
// 类似于火车车厢拼接了,违反得墨忒⽿律
final String outputDir =
ctxt.getOptions().getScratchDir().getAbsolutePath();
优化后
public String getScratchDirectoryOption() {
Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();
return outputDir;
}
重要事项总结
注释
1. 避免不恰当的信息
2. 删掉废弃的注释
3. 去掉冗余注释
4. 去掉糟糕的注释
5. 删掉注释掉的代码
环境
1. 用单步的小操作构建系统
2. 应当能够发出单个指令就可以运⾏全部单元测试
方法
1. 避免过多的参数
2. 尽量减少出参,如要修改什么,可以在入参中对象修改
3. 尽量避免入参是Boolean的标识
4. 永不调用的方法要直接删除
JAVA
1. 通过使⽤通配符避免过⻓的导⼊清单,指的是import package.*; 这一点与阿里规范相反,个人更喜欢阿里规范
2. 不要继承常量
3. 相同类型的,用枚举来代替常量
名称
1. 采⽤描述性名称,要舍得花更多的时间命名,不要怕长
2. 名称应与抽象层级相符,方法名和类名相关联
3. 尽可能使⽤标准命名法,而不是缩写
4. 避免使用带有歧义或容易混淆的命名
5. 避免命名中带有编码性的前缀,如m_ , f_
6. 方法的命名就能说明该作用,避免同一个方法里做多件事
测试
1. 一定要单元测试,要涵盖场景,避免测试不足
2. 覆盖率⼯具能汇报你测试策略中的缺⼝。使⽤覆盖率⼯具能更容 易地找到测试不⾜的模块、类和函数
3. 特别注意测试边界条件。
4. 全⾯测试相近的缺陷
5. 测试失败的模式有启发性
6. 测试应该快速,优化效率
其他问题
1. 同一份代码文件或文件夹不应存在不同语言的文件
2. 考虑代码安全性,边界问题,线程安全
3. 抽象或抽取工具类,避免重复代码
4. 方法里确定执行不到的代码应该删除
5. 垂直分隔,按规则组织类中的变量,方法等,合理使用换行
6. 避免命名前后不一致
7. 不互相依赖的东⻄不该耦合
8. 用静态方法代替构造函数
9. 变量在命名的时候就要表达出他的意图。Date daysLater = date.add(5);
10. ⽤多态替代If/Else或Switch/Case
11. ⽤命名常量替代魔术数
12. if语句封装条件,例如:if (shouldBeDeleted(timer))
13. 尽可能将条件表⽰为肯定 形式。例如: if (buffer.shouldCompact()) 要好于 if (!buffer.shouldNotCompact())
14. 单个方法只做一件事
15. 几个方法带有执行先后顺序的,尽量用返回值或者抽出单独方法。如:
public class MoogDiver {
Gradient gradient;
List<Spline> splines;
public void dive(String reason) {
Gradient gradient = saturateGradient();
List<Spline> splines = reticulateSplines(gradient);
diveForMoog(splines, reason);
...
}
}
16. 应尽量判断入参,返回值 边界条件与null
17. 一个类用到父类的常量,一定要写父类名.常量。