代码美学

最近我写的代码被同事code review时一直被吐槽,而我发现确实存在各种各样的问题,所以我打算把这些都给记录下来,以后不断改进

1.正确的命名

有一句老话说的好:计算机科学有两件难事,一是缓存失效,二是取名。我对第二点深有体会,所以我记录了一下取名的一些规范

  • 尽量避免使用单个字母去给变量命名,这会使变量失去相关信息
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public 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
    10
    int 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
    5
    public sealed class People permits Student, Teacher {
    // 前面都已经类型约束了为什么还要写xxxxStr?
    private String userNameStr;
    private String passWordStr;
    }
  • 适当的给参数加上单位,比如下边两个哪个更好理解?
    1
    2
    3
    4
    5
    6
    7
    public void executor(int delay){
    // TODO
    }

    public void executor(int delaySeconds){
    // TODO
    }
  • 不要在基类的取名上使用BaseXXX,这样的名字并不好
    1
    2
    3
    4
    5
    6
    7
    class BaseTruck {

    }

    class Truck extends BaseTruck {
    // Truck总是让人一头雾水
    }
    1
    2
    3
    4
    5
    6
    7
    class Truck {

    }

    class TrailerTruck extends Truck {
    // 更加的具体
    }

2.勿写注释(不是指接口文档、方法文档等各种文档哦)

这是一个充满争议的问题,其实大部分时间我更加认为注释是告诉我们为什么要这样做而不是解释这么做代表什么意思,其实我们按照上边命名约束做好命名之后就可以通过名称解释大部分代码了。最主要的是有时候我们需要更改代码,但是会发生代码更改了注释没更改的情况,这种情况一旦发生会让读代码的人很头疼的。

1
2
3
4
5
6
7
8
9
// 如果状态是5代表消息已经发送
if (status == 5) {
message.markSend();
}

final int MESSAGE_SEND = 5;
if (status == MESSAGE_SEND) { // 这句代码读起来就像注释
message.markSend();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 如果消息发送者是当前用户,同时消息是五分钟内发送的,或者当前用户是管理员就可以更新消息
if ((message.user.id == current_user.id && ((System.currentTimeMillis() - message.delivered_time()) < 300000 )) or current_user.role == Role.Admin) {
message.update_text(text);
}

// after
boolean user_is_author = message.user.id == current_user.id;
final long FIVE_MINUTES = 5 * 60 * 1000;
boolean is_recent = (System.currentTimeMillis() - message.delivered_time()) < FIVE_MINUTES;
if ((user_is_author && is_recent) || (current_user.role == Role.Admin)) {
message.update_text(text);
}

// final
public boolean can_edit_message(Message message, User current_user) {
boolean user_is_author = message.user.id == current_user.id;
final long FIVE_MINUTES = 5 * 60 * 1000;
boolean is_recent = (System.currentTimeMillis() - message.delivered_time()) > FIVE_MINUTES;
return (user_is_author && is_recent) || current_user.role == Role.Admin;
}

if (can_edit_message(message, current_user)) {
message.update_text(text);
}

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
    22
    public 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
    35
    public 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) {
    }
    }
    第一步:接口分层,把接口分为外部和内部接口,所有空值判断放在外部接口完成,只处理一次;而内部接口传入的变量由外部接口保证不为空,从而减少空值判断
    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
    public 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, "不支持的分享类型");
    }
    }
    }
    第二步利用多态,每种业务单独处理,在接口不再做任何业务判断。把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
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    abstract 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
    12
    public 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Getter
public class RecodeHashSet<E> extends HashSet<E> {

private int addCount = 0;


@Override
public boolean add(E e) {
addCount += 1;
return super.add(e);
}

@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}

}
1
2
3
4
5
6
@Test
public void testRecodeSet() {
RecodeHashSet<String> set = new RecodeHashSet<>();
set.addAll(List.of("zs", "ls"));
assert set.getAddCount() == 2; // 失败了,addCount == 4?
}

为什么会失败?让人很迷惑,探究源码之后我们发现addAll()会调用add()方法,因为会先调用自己的add()方法所以发生了重复计数,我们只需要把addAll()方法的计算给删除就好了

1
2
3
4
5
6
7
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}

虽然删除之后问题解决了但是这样真的合适吗,是不是需要在我们极度熟悉父类源码的情况下才能发现问题,如果有一天HashSet的add()方法不再调用add()方法我们的RecodeSet是不是又会出现问题,所以为了避免父类的方法具体实现对我们带来影响我们应该使用组合而非继承。

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
@AllArgsConstructor
public class ForwardingSet<E> implements Set<E> {

private final Set<E> set;

@Override
public int size() {
return set.size();
}

@Override
public boolean isEmpty() {
return set.isEmpty();
}

@Override
public boolean contains(Object o) {
return set.contains(o);
}

@Override
public Iterator<E> iterator() {
return set.iterator();
}

@Override
public Object[] toArray() {
return set.toArray();
}

@Override
public <T> T[] toArray(T[] a) {
return set.toArray(a);
}

@Override
public boolean add(E e) {
return set.add(e);
}

@Override
public boolean remove(Object o) {
return set.remove(o);
}

@Override
public boolean containsAll(Collection<?> c) {
return set.containsAll(c);
}

@Override
public boolean addAll(Collection<? extends E> c) {
return set.addAll(c);
}

@Override
public boolean retainAll(Collection<?> c) {
return set.retainAll(c);
}

@Override
public boolean removeAll(Collection<?> c) {
return set.removeAll(c);
}

@Override
public void clear() {
set.clear();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Getter
public class RecodeHashSet<E> extends ForwardingSet<E> {

private int addCount = 0;

public RecodeHashSet(Set<E> set) {
super(set);
}

public boolean add(E e) {
addCount += 1;
return super.add(e);
}

public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}

}
1
2
3
4
5
6
7
@Test
public void testRecodeSet() {
// 不光能传入hashset,还能传入任何set类型,并不会让父类的行为影响我们
RecodeHashSet<String> set = new RecodeHashSet<>(new HashSet<>());
set.addAll(List.of("zs", "ls"));
assert set.getAddCount() == 2;
}

5.不要过度抽象

我们提高代码复用常用的一个套路就是识别重复的代码进行抽象,然后继承,我们容易误入的一个怪圈就是抽象越多越好,但是过度抽象带给我们的就是耦合,我们再去清理对象关系的时候只能看到错综复杂的关系网,如果我们一直不抽象那么就会给我们带来一写重复代码,所以把我抽象的程度很重要

比如我现在有一个游戏系统,我想把用户所有的数据保存到XML文件中,我就会写这样的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class SaveGameData {

private String fileName;

public SaveGameData(String fileName) {
this.fileName = fileName;
}

public void save(GameStat stat) {
// 1.生成一个格式化转化工具,将stat转xml
// 2.将xml转成字节流
// 3.写入文件
}

public GameStat load() {
// 1.读取文件
// 2.将字节流转成xml
// 3.将xml转成GameStat
return null;
}
}

现在我需要增加对json格式的支持,怎么办?改造

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
public class SaveGameData {

private SaveMode saveMode;

public enum SaveMode {
XML,
JSON;
}

public SaveGameData(SaveMode saveMode) {
this.saveMode = saveMode;
}

public void save(GameStat stat, String fileName) {
if (this.saveMode == SaveMode.XML) {
SaveXml saveXml = new SaveXml(fileName);
saveXml.save();
}
if (this.saveMode == SaveMode.JSON) {
SaveJson saveJson = new SaveJson(fileName);
saveJson.save();
}
}

public GameStat load(String fileName) {
if (this.saveMode == SaveMode.XML) {
SaveXml saveXml = new SaveXml(fileName);
return saveXml.load();
}
if (this.saveMode == SaveMode.JSON) {
SaveJson saveJson = new SaveJson(fileName);
return saveJson.load();
}
return null;
}
}

class GameStat {
private Map<String, String> userData;
}


class SaveXml {
public SaveXml(String fileName) {
this.fileName = fileName;
}

public void save() {}

public GameStat load() {return null;}
}

class SaveJson {
public SaveJson(String fileName) {
this.fileName = fileName;
}

public void save() {}

public GameStat load() {return null;}
}

当我们改完之后我们编译器告诉我们有大量判断代码是重复,这个时候我们开始思考是否需要抽象?然后开始抽象得到

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
public class SaveGameData {

private SaveMode saveMode;

public enum SaveMode {
XML,
JSON;
}

public SaveGameData(SaveMode saveMode) {
this.saveMode = saveMode;
}

public void save(GameStat stat, String fileName) {
FileSave save = null;
if (this.saveMode == SaveMode.XML) {
save = new SaveXml(fileName);
}
if (this.saveMode == SaveMode.JSON) {
save = new SaveJson(fileName);
}
save.save(stat);
}

public GameStat load(String fileName) {
FileSave load = null;
if (this.saveMode == SaveMode.XML) {
load = new SaveXml(fileName);
}
if (this.saveMode == SaveMode.JSON) {
load = new SaveJson(fileName);
}
return load.load();
}
}

class GameStat {
private Map<String, String> userData;
}

class FileSave {
private String fileName;

public FileSave(String fileName) {
this.fileName = fileName;
}

public void save(GameStat stat) {}

public GameStat load() {return null;}
}

class SaveXml extends FileSave {
public SaveXml(String fileName) {
super(fileName);
}

public void save(GameStat stat) {}

public GameStat load() {return null;}
}

class SaveJson extends FileSave {
public SaveJson(String fileName) {
super(fileName);
}

public void save(GameStat stat) {}

public GameStat load() {return null;}
}

看上去很完美,但是其实相比较写两个SaveXml和SaveJson不继承FileSave只是少写了一行private String fileName;而已,但是我们发现强行抽象之后其实它已经和文件有了强耦合,如果之后我们想保存到数据库或者S3或者远程服务器呢?而且我们的save方法与GameStat也绑定了,所以这并不是一个好的抽象,也不是我们真正需要的抽象,或许当我们需要保存更多介质更多保存的类型更多的时候我们可以抽象出来两个顶级接口,而不是现在就提早抽象了。


代码美学
https://vegetablest.github.io/2022/08/13/code_aesthetics/
作者
af su
发布于
2022年8月13日
许可协议