代码美学
最近我写的代码被同事code review时一直被吐槽,而我发现确实存在各种各样的问题,所以我打算把这些都给记录下来,以后不断改进
1.正确的命名
有一句老话说的好:计算机科学有两件难事,一是缓存失效,二是取名。我对第二点深有体会,所以我记录了一下取名的一些规范
- 尽量避免使用单个字母去给变量命名,这会使变量失去相关信息
1
2
3
4
5
6
7
8
9
10public sealed class People permits Student, Teacher {
// before
private String n;
private String p;
private Integer a;
//after
private String userName;
private String passWord;
private Integer age;
} - 绝对不要使用缩写,因为缩写的含义依赖于上下文,尽量避免让别人读代码时候依赖上下文
1
2
3
4
5
6
7
8
9
10int price_count_reader; // 无缩写
int num_errors; // "num" 是一个常见的写法
int num_dns_connections; // 人人都知道 "DNS" 是什么
int n; // 毫无意义.
int nerr; // 含糊不清的缩写.
int n_comp_conns; // 含糊不清的缩写.
int wgc_connections; // 只有贵团队知道是什么意思.
int pc_reader; // "pc" 有太多可能的解释了.
int cstmr_id; // 删减了若干字母. - 不要在命名中参杂变量类型信息,比如passWordStr、userNameStr
1
2
3
4
5public sealed class People permits Student, Teacher {
// 前面都已经类型约束了为什么还要写xxxxStr?
private String userNameStr;
private String passWordStr;
} - 适当的给参数加上单位,比如下边两个哪个更好理解?
1
2
3
4
5
6
7public void executor(int delay){
// TODO
}
public void executor(int delaySeconds){
// TODO
} - 不要在基类的取名上使用BaseXXX,这样的名字并不好
1
2
3
4
5
6
7class BaseTruck {
}
class Truck extends BaseTruck {
// Truck总是让人一头雾水
}1
2
3
4
5
6
7class Truck {
}
class TrailerTruck extends Truck {
// 更加的具体
}
2.勿写注释(不是指接口文档、方法文档等各种文档哦)
这是一个充满争议的问题,其实大部分时间我更加认为注释是告诉我们为什么要这样做而不是解释这么做代表什么意思,其实我们按照上边命名约束做好命名之后就可以通过名称解释大部分代码了。最主要的是有时候我们需要更改代码,但是会发生代码更改了注释没更改的情况,这种情况一旦发生会让读代码的人很头疼的。
1 |
|
1 |
|
3.不要过度嵌套
if else作为每种编程语言都不可或缺的条件语句,我们在编程时会大量的用到。但if else一般不建议嵌套超过三层,如果一段代码存在过多的if else嵌套,代码的可读性就会急速下降,后期维护难度也大大提高。所以,不要超过三层!不要超过三层!!不要超过三层!!!
减少代码嵌套的手段一般是抽取和反转,反转是指提前返回,抽取往往指提炼方法。
- 反转条件,将负面条件移到前面,使方法尽早返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public int calculate(int top, int bottom) {
if (top > bottom) {
int sum = 0;
for (int number = bottom; number <= top; number++) {
sum += number;
}
return sum;
} else {
return 0;
}
}
public int calculate(int top, int bottom) {
if (top < bottom) {
return 0;
}
int sum = 0;
for (int number = bottom; number <= top; number++) {
sum += number;
}
return sum;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35public void registerUser(String user) {
String[] parts = user.split(":");
if (parts.length == 2) {
int userId = Integer.parseInt(parts[0]);
if (userId >= 0) {
String userName = parts[1];
if (users.containsKey(userId)) {
users.get(userId).setUserName(userName);
} else {
users.put(userId, new User(userName));
}
} else {
throw new IllegalArgumentException("Invalid userId : " + userId);
}
} else {
throw new IllegalArgumentException("Invalid user string: " + user);
}
}
// 反转条件,正确的分支都下沉,异常的分支都上浮
public void registerUser(String user) {
String[] parts = user.split(":");
if (parts.length != 2) {
throw new IllegalArgumentException("Invalid user string: " + user);
}
int userId = Integer.parseInt(parts[0]);
if (userId < 0) {
throw new IllegalArgumentException("Invalid userId : " + userId);
}
String userName = parts[1];
if (users.containsKey(userId)) {
users.get(userId).setUserName(userName);
} else {
users.put(userId, new User(userName));
}
} - 抽取方法,合理使用设计模式,以一个分享功能为例第一步:接口分层,把接口分为外部和内部接口,所有空值判断放在外部接口完成,只处理一次;而内部接口传入的变量由外部接口保证不为空,从而减少空值判断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88// 分享类型
public interface ShareType {
int TYPE_LINK = 0;
int TYPE_IMAGE = 1;
int TYPE_TEXT = 2;
int TYPE_IMAGE_TEXT = 3;
}
// 分享元素
class ShareItem {
int type;
String title;
String content;
String imagePath;
String link;
}
// 分享回调
interface ShareListener {
int STATE_SUCC = 0;
int STATE_FAIL = 1;
void onCallback(int state, String msg);
}
public class Share {
// 分享
public void share (ShareItem item, ShareListener listener) {
if (item != null) {
if (item.type == ShareType.TYPE_LINK) {
// 分享链接
if (Strings.isNotBlank(item.link) && Strings.isNotBlank(item.content)) {
doShareLink(item.link, item.title, item.content, listener);
} else {
if (listener != null) {
listener.onCallback(ShareListener.STATE_FAIL, "分享信息不完整");
}
}
} else if (item.type == ShareType.TYPE_IMAGE) {
// 分享图片
if (Strings.isNotBlank(item.imagePath)) {
doShareImage(item.imagePath, listener);
} else {
if (listener != null) {
listener.onCallback(ShareListener.STATE_FAIL, "分享信息不完整");
}
}
} else if (item.type == ShareType.TYPE_TEXT) {
// 分享文本
if (Strings.isNotBlank(item.content)) {
doShareText(item.content, listener);
} else {
if (listener != null) {
listener.onCallback(ShareListener.STATE_FAIL, "分享信息不完整");
}
}
} else if (item.type == ShareType.TYPE_IMAGE_TEXT) {
// 分享图文
if (Strings.isNotBlank(item.imagePath) && Strings.isNotBlank(item.content)) {
doShareImageAndText(item.imagePath, item.content, listener);
} else {
if (listener != null) {
listener.onCallback(ShareListener.STATE_FAIL, "分享信息不完整");
}
}
} else {
if (listener != null) {
listener.onCallback(ShareListener.STATE_FAIL, "不支持的分享类型");
}
}
} else {
if (listener != null) {
listener.onCallback(ShareListener.STATE_FAIL, "ShareItem 不能为 null");
}
}
}
// 具体分享功能实现
private void doShareImageAndText(String imagePath, String content, ShareListener listener) {
}
private void doShareText(String content, ShareListener listener) {
}
private void doShareImage(String imagePath, ShareListener listener) {
}
private void doShareLink(String link, String title, String content, ShareListener listener) {
}
}第二步利用多态,每种业务单独处理,在接口不再做任何业务判断。把ShareItem抽象出来,作为基础类,然后针对每种业务各自实现其子类:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56public void share (ShareItem item, ShareListener listener) {
if (item == null) {
if (listener != null) {
listener.onCallback(ShareListener.STATE_FAIL, "ShareItem 不能为 null");
}
return;
}
if (listener == null) {
listener = (state, msg) -> log.debug("ShareListener is null");
}
shareImpl(item, listener);
}
private void shareImpl(ShareItem item, ShareListener listener) {
if (item.type == ShareType.TYPE_LINK) {
// 分享链接
if (Strings.isNotBlank(item.link) && Strings.isNotBlank(item.content)) {
doShareLink(item.link, item.title, item.content, listener);
} else {
if (listener != null) {
listener.onCallback(ShareListener.STATE_FAIL, "分享信息不完整");
}
}
} else if (item.type == ShareType.TYPE_IMAGE) {
// 分享图片
if (Strings.isNotBlank(item.imagePath)) {
doShareImage(item.imagePath, listener);
} else {
if (listener != null) {
listener.onCallback(ShareListener.STATE_FAIL, "分享信息不完整");
}
}
} else if (item.type == ShareType.TYPE_TEXT) {
// 分享文本
if (Strings.isNotBlank(item.content)) {
doShareText(item.content, listener);
} else {
if (listener != null) {
listener.onCallback(ShareListener.STATE_FAIL, "分享信息不完整");
}
}
} else if (item.type == ShareType.TYPE_IMAGE_TEXT) {
// 分享图文
if (Strings.isNotBlank(item.imagePath) && Strings.isNotBlank(item.content)) {
doShareImageAndText(item.imagePath, item.content, listener);
} else {
if (listener != null) {
listener.onCallback(ShareListener.STATE_FAIL, "分享信息不完整");
}
}
} else {
if (listener != null) {
listener.onCallback(ShareListener.STATE_FAIL, "不支持的分享类型");
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71abstract class ShareItem {
int type;
public ShareItem(int type) {
this.type = type;
}
public abstract void doShare(ShareListener listener);
}
class LinkShareItem extends ShareItem {
String title;
String content;
String link;
public LinkShareItem(String link, String title, String content) {
super(ShareType.TYPE_LINK);
this.title = Strings.isNotBlank(title) ? title : "default";
this.content = Strings.isNotBlank(content) ? content : "default";
this.link = Strings.isNotBlank(link) ? link : "default";
}
@Override
public void doShare(ShareListener listener) {
// do share
}
}
class ImageShareItem extends ShareItem {
String imagePath;
public ImageShareItem(String imagePath) {
super(ShareType.TYPE_IMAGE);
this.imagePath = Strings.isNotBlank(imagePath) ? imagePath : "default";
}
@Override
public void doShare(ShareListener listener) {
// do share
}
}
class TextShareItem extends ShareItem {
String content;
public TextShareItem(String content) {
super(ShareType.TYPE_TEXT);
this.content = Strings.isNotBlank(content) ? content : "default";
}
@Override
public void doShare(ShareListener listener) {
// do share
}
}
class ImageTextShareItem extends ShareItem {
String content;
String imagePath;
public ImageTextShareItem(String imagePath, String content) {
super(ShareType.TYPE_IMAGE_TEXT);
this.imagePath = Strings.isNotBlank(imagePath) ? imagePath : "default";
this.content = Strings.isNotBlank(content) ? content : "default";
}
@Override
public void doShare(ShareListener listener) {
// do share
}
}1
2
3
4
5
6
7
8
9
10
11
12public void share (ShareItem item, ShareListener listener) {
if (item == null) {
if (listener != null) {
listener.onCallback(ShareListener.STATE_FAIL, "ShareItem 不能为 null");
}
return;
}
if (listener == null) {
listener = (state, msg) -> log.debug("ShareListener is null");
}
item.doShare(listener);
}
4.组合优于继承
面向对象编程中,有一条非常经典的设计原则,那就是:组合优于继承,多用组合少用继承。同样地,在《阿里巴巴Java开发手册》中有一条规定:谨慎使用继承的方式进行扩展,优先使用组合的方式实现。
每个人在刚刚学习面向对象编程时都会觉得:继承(用来表示类之间的is-a关系)可以实现类的复用。所以,很多开发人员在需要复用一些代码的时候会很自然的使用类的继承的方式,因为书上就是这么写的,官方是这么教的,所以作为面向对象四大特性之一的继承,被我们作为解决代码复用的主要手段之一。虽然继承有诸多作用,但继承层次过深、过复杂,也会影响到代码的可维护性。下边我们就用一个测试来表现它带来的弊端
1 |
|
1 |
|
为什么会失败?让人很迷惑,探究源码之后我们发现addAll()会调用add()方法,因为会先调用自己的add()方法所以发生了重复计数,我们只需要把addAll()方法的计算给删除就好了
1 |
|
虽然删除之后问题解决了但是这样真的合适吗,是不是需要在我们极度熟悉父类源码的情况下才能发现问题,如果有一天HashSet的add()方法不再调用add()方法我们的RecodeSet是不是又会出现问题,所以为了避免父类的方法具体实现对我们带来影响我们应该使用组合而非继承。
1 |
|
1 |
|
1 |
|
5.不要过度抽象
我们提高代码复用常用的一个套路就是识别重复的代码进行抽象,然后继承,我们容易误入的一个怪圈就是抽象越多越好,但是过度抽象带给我们的就是耦合,我们再去清理对象关系的时候只能看到错综复杂的关系网,如果我们一直不抽象那么就会给我们带来一写重复代码,所以把我抽象的程度很重要
比如我现在有一个游戏系统,我想把用户所有的数据保存到XML文件中,我就会写这样的代码
1 |
|
现在我需要增加对json格式的支持,怎么办?改造
1 |
|
当我们改完之后我们编译器告诉我们有大量判断代码是重复,这个时候我们开始思考是否需要抽象?然后开始抽象得到
1 |
|
看上去很完美,但是其实相比较写两个SaveXml和SaveJson不继承FileSave只是少写了一行private String fileName;而已,但是我们发现强行抽象之后其实它已经和文件有了强耦合,如果之后我们想保存到数据库或者S3或者远程服务器呢?而且我们的save方法与GameStat也绑定了,所以这并不是一个好的抽象,也不是我们真正需要的抽象,或许当我们需要保存更多介质更多保存的类型更多的时候我们可以抽象出来两个顶级接口,而不是现在就提早抽象了。