编程技巧

把代码分割成更小的单元块

大部分人阅读代码的习惯都是,先看整体再看细节。所以,我们要有模块化和抽象思维,善于将大块的复杂逻辑提炼成类或者函数,屏蔽掉细节,让阅读代码的人不至于迷失在细节中,这样能极大地提高代码的可读性。不过,只有代码逻辑比较复杂的时候,我们其实才建议提炼类或者函数。毕竟如果提炼出的函数只包含两三行代码,在阅读代码的时候,还得跳过去看一下,这样反倒增加了阅读成本。

这里我举一个例子来进一步解释一下。重构前,在 invest() 函数中,最开始的那段关于时间处理的代码,是不是很难看懂?重构之后,我们将这部分逻辑抽象成一个函数,并且命名为 isLastDayOfMonth,从名字就能清晰地了解它的功能,判断今天是不是当月的最后一天。这里,我们就是通过将复杂的逻辑代码提炼成函数,大大提高了代码的可读性。代码具体如下所示:

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
// 重构前的代码
public void invest(long userId, long financialProductId)
{
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.set(Calendar.DATE, (calendar.get(Calendar.DATE) + 1));
if (calendar.get(Calendar.DAY_OF_MONTH) == 1)
{
return;
}
//...
}

// 重构后的代码:提炼函数之后逻辑更加清晰
public void invest(long userId, long financialProductId)
{
if (isLastDayOfMonth(new Date()))
{
return;
}
//...
}

public boolean isLastDayOfMonth(Date date)
{
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.set(Calendar.DATE, (calendar.get(Calendar.DATE) + 1));
if (calendar.get(Calendar.DAY_OF_MONTH) == 1)
{
return true;
}
return false;
}

避免函数参数过多

函数包含 3、4 个参数的时候还是能接受的,大于等于 5 个的时候,我们就觉得参数有点过多了,会影响到代码的可读性,使用起来也不方便。针对参数过多的情况,一般有 2 种处理方法:

  • 考虑函数是否职责单一,是否能通过拆分成多个函数的方式来减少参数。示例代码如下所示:

    1
    2
    3
    4
    5
    6
    public User getUser(String username, String telephone, String email);

    // 拆分成多个函数
    public User getUserByUsername(String username);
    public User getUserByTelephone(String telephone);
    public User getUserByEmail(String email);
  • 将函数的参数封装成对象。示例代码如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public void postBlog(String title, String summary, String keywords, String content, String category, long authorId);

    // 将参数封装成对象
    public class Blog
    {
    private String title;
    private String summary;
    private String keywords;
    private String content;
    private String category;
    private long authorId;
    }
    public void postBlog(Blog blog);

除此之外,如果函数是对外暴露的远程接口,将参数封装成对象,还可以提高接口的兼容性。在往接口中添加新的参数的时候,老的远程接口调用者有可能就不需要修改代码来兼容新的接口了。

勿用函数参数来控制逻辑

不要在函数中使用布尔类型的标识参数来控制内部逻辑,true 的时候走这块逻辑,false 的时候走另一块逻辑。这明显违背了单一职责原则和接口隔离原则。我建议将其拆成两个函数,可读性上也要更好:

1
2
3
4
5
public void buyCourse(long userId, long courseId, boolean isVip);

// 将其拆分成两个函数
public void buyCourse(long userId, long courseId);
public void buyCourseForVip(long userId, long courseId);

不过,如果函数是 private 私有函数,影响范围有限,或者拆分之后的两个函数经常同时被调用,我们可以酌情考虑保留标识参数。示例代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 拆分成两个函数的调用方式
boolean isVip = false;
//...省略其他逻辑...
if (isVip)
{
buyCourseForVip(userId, courseId);
}
else
{
buyCourse(userId, courseId);
}

// 保留标识参数的调用方式更加简洁
boolean isVip = false;
//...省略其他逻辑...
buyCourse(userId, courseId, isVip);

除了布尔类型作为标识参数来控制逻辑的情况外,还有一种根据参数是否为 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
public List<Transaction> selectTransactions(Long userId, Date startDate, Date endDate) 
{
if (startDate != null && endDate != null)
{
// 查询两个时间区间的 transactions
}
if (startDate != null && endDate == null)
{
// 查询 startDate 之后的所有 transactions
}
if (startDate == null && endDate != null)
{
// 查询 endDate 之前的所有 transactions
}
if (startDate == null && endDate == null)
{
// 查询所有的 transactions
}
}

// 拆分成多个 public 函数,更加清晰、易用
private List<Transaction> selectTransactions(Long userId, Date startDate, Date endDate)
{
//...
}

public List<Transaction> selectTransactionsBetween(Long userId, Date startDate, Date endDate)
{
return selectTransactions(userId, startDate, endDate);
}

public List<Transaction> selectTransactionsStartWith(Long userId, Date startDate)
{
return selectTransactions(userId, startDate, null);
}

public List<Transaction> selectTransactionsEndWith(Long userId, Date endDate)
{
return selectTransactions(userId, null, endDate);
}

public List<Transaction> selectAllTransactions(Long userId)
{
return selectTransactions(userId, null, 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
public boolean checkUserIfExisting(String telephone, String username, String email)  
{
if (!StringUtils.isBlank(telephone))
{
User user = userRepo.selectUserByTelephone(telephone);
return user != null;
}

if (!StringUtils.isBlank(username))
{
User user = userRepo.selectUserByUsername(username);
return user != null;
}

if (!StringUtils.isBlank(email))
{
User user = userRepo.selectUserByEmail(email);
return user != null;
}

return false;
}

// 拆分成三个函数
public boolean checkUserIfExistingByTelephone(String telephone);
public boolean checkUserIfExistingByUsername(String username);
public boolean checkUserIfExistingByEmail(String email);

移除过深的嵌套层次

代码嵌套层次过深往往是因为 if-else、switch-case、for 循环过度嵌套导致的。我个人建议,嵌套最好不超过两层,超过两层之后就要思考一下是否可以减少嵌套。过深的嵌套本身理解起来就比较费劲,除此之外,嵌套过深很容易因为代码多次缩进,导致嵌套内部的语句超过一行的长度而折成两行,影响代码的整洁。解决嵌套过深的方法也比较成熟,有下面 4 种常见的思路:

  • 去掉多余的 if 或 else 语句。代码示例如下所示:

    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
    // 示例一
    public double calculateTotalAmount(List<Order> orders)
    {
    if (orders == null || orders.isEmpty())
    {
    return 0.0;
    }
    else // 此处的 else 可以去掉
    {
    double amount = 0.0;
    for (Order order : orders)
    {
    if (order != null)
    {
    amount += (order.getCount() * order.getPrice());
    }
    }
    return amount;
    }
    }

    // 示例二
    public List<String> matchStrings(List<String> strList, String subStr)
    {
    List<String> matchedStrings = new ArrayList<>();
    if (strList != null && subStr != null)
    {
    for (String str : strList)
    {
    if (str != null) // 跟下面的 if 语句可以合并在一起
    {
    if (str.contains(subStr))
    {
    matchedStrings.add(str);
    }
    }
    }
    }
    return matchedStrings;
    }
  • 使用编程语言提供的 continue、break、return 关键字,提前退出嵌套。代码示例如下所示:

    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
    // 重构前的代码
    public List<String> matchStrings(List<String> strList, String subStr)
    {
    List<String> matchedStrings = new ArrayList<>();
    if (strList != null && subStr != null)
    {
    for (String str : strList)
    {
    if (str != null && str.contains(subStr))
    {
    matchedStrings.add(str);
    // 此处还有 10 行代码...
    }
    }
    }
    return matchedStrings;
    }

    // 重构后的代码:使用 continue 提前退出
    public List<String> matchStrings(List<String> strList, String subStr)
    {
    List<String> matchedStrings = new ArrayList<>();
    if (strList != null && subStr != null)
    {
    for (String str : strList)
    {
    if (str == null || !str.contains(subStr))
    {
    continue;
    }
    matchedStrings.add(str);
    // 此处还有 10 行代码...
    }
    }
    return matchedStrings;
    }
  • 调整执行顺序来减少嵌套。具体的代码示例如下所示:

    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
    // 重构前的代码
    public List<String> matchStrings(List<String> strList, String subStr)
    {
    List<String> matchedStrings = new ArrayList<>();
    if (strList != null && subStr != null)
    {
    for (String str : strList)
    {
    if (str != null)
    {
    if (str.contains(subStr))
    {
    matchedStrings.add(str);
    }
    }
    }
    }
    return matchedStrings;
    }

    // 重构后的代码:先执行判空逻辑,再执行正常逻辑
    public List<String> matchStrings(List<String> strList, String subStr)
    {
    if (strList == null || subStr == null) // 先判空
    {
    return Collections.emptyList();
    }

    List<String> matchedStrings = new ArrayList<>();
    for (String str : strList)
    {
    if (str != null)
    {
    if (str.contains(subStr))
    {
    matchedStrings.add(str);
    }
    }
    }
    return matchedStrings;
    }
  • 将部分嵌套逻辑封装成函数调用,以此来减少嵌套。具体的代码示例如下所示:

    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
    // 重构前的代码
    public List<String> appendSalts(List<String> passwords)
    {
    if (passwords == null || passwords.isEmpty())
    {
    return Collections.emptyList();
    }

    List<String> passwordsWithSalt = new ArrayList<>();
    for (String password : passwords)
    {
    if (password == null)
    {
    continue;
    }
    if (password.length() < 8)
    {
    //...
    }
    else
    {
    //...
    }
    }
    return passwordsWithSalt;
    }

    // 重构后的代码:将部分逻辑抽成函数
    public List<String> appendSalts(List<String> passwords)
    {
    if (passwords == null || passwords.isEmpty())
    {
    return Collections.emptyList();
    }

    List<String> passwordsWithSalt = new ArrayList<>();
    for (String password : passwords)
    {
    if (password == null)
    {
    continue;
    }
    passwordsWithSalt.add(appendSalt(password));
    }
    return passwordsWithSalt;
    }

    private String appendSalt(String password)
    {
    String passwordWithSalt = password;
    if (password.length() < 8)
    {
    //...
    }
    else
    {
    //...
    }
    return passwordWithSalt;
    }

除此之外,常用的还有通过使用多态来替代 if-else、switch-case 条件判断的方法。这个思路涉及代码结构的改动。

学会使用解释性变量

常用的用解释性变量来提高代码的可读性的情况有下面 2 种:

  • 常量取代魔法数字。示例代码如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public double CalculateCircularArea(double radius) 
    {
    return (3.1415) * radius * radius;
    }

    // 常量替代魔法数字
    public static final Double PI = 3.1415;
    public double CalculateCircularArea(double radius)
    {
    return PI * radius * radius;
    }
  • 使用解释性变量来解释复杂表达式。示例代码如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    if (date.after(SUMMER_START) && date.before(SUMMER_END)) 
    {
    //...
    }
    else
    {
    //...
    }

    // 引入解释性变量后逻辑更加清晰
    boolean isSummer = date.after(SUMMER_START) && date.before(SUMMER_END);
    if (isSummer)
    {
    //...
    }
    else
    {
    //...
    }