0%

《代码整洁之道》读后感

背景

​ 公司的测试工具平台开发也快一年了,积累的功能也越来越多,最近刚好需要对测试用例平台模块的相关功能进行修改。 修改的过程真的是痛苦。。。虽然原来已经看过一遍clean code了,但时间久了又回到了老样子。。。

我们趟过代码的水域。我们穿过灌木密布、瀑布暗藏的沼泽地。我们拼命想找到出路,期望有点什么线索能启发我们到底发生了什么事;但目光所及,只是越来越多死气沉沉的代码。 一一 代码整洁之道

​ 整本书看完后,并不觉得所有的东西都一定要按照书中的来做,但有以下的一些经验还是可以参考和采纳的。

有意义的命名

名副其实

选个好名字要花时间,但省下来的时间比花掉的多。注意命名,而且一旦发现有更好的名称,就换掉旧的。如果名称需要注释来补充,那就不算是名副其实

错误实例

1
int d; // 消逝的时间,以日计

正确实例

1
2
3
int elapsedTimeInDays; 
int daysSinceCreation;
int daysSinceModification;

避免误导

别用accountList来指称一组账号,除非它真的是List类型。所以,用accountGroup或bunchOfAccounts,甚至直接用accounts都会好一些。 特别是用小写字母l和大写字母O作为变量名。

错误实例

1
2
3
4
5
int a = l;
if (O == l)
a = O1;
else
l = 01;

做有意义的区分

以数字系列命名(a1、a2,……aN)是依义命名的对立面。这样的名称纯属误导——完全没有提供正确信息

错误实例

1
2
3
4
5
public static void copyChars(char a1[], char a2[]) { 
for (int i = 0; i < a1.length; i++) {
a2[i] = a1[i];
}
}

如果 参数名改为source和destination,这个函数就会像样许多。

使用能读出来的名称

尽量避免使用缩写。。。大部分情况,没人知道你这个缩写是怎么缩的。

错误实例

1
2
3
4
5
class DtaRcrd102 { 
private Date genymdhms;
private Date modymdhms;
private final String pszqint = "102"; /* ... */
};

正确实例

1
2
3
4
5
class Customer {
private Date generationTimestamp;
private Date modificationTimestamp;
private final String recordId = "102"; /* ... */
};

使用可搜索的名称

很多时候不一定记得方法或者变量名在哪个地方,可能需要通过搜索关键词来进行搜索。此时找STUDENT_ID很容易,但想找数字7就麻烦了。

错误实例

1
for (int j=0; j<34; j++) { s += (t[j]*4)/5; }

正确实例

1
2
int realDaysPerIdealDay = 4; 
const int WORK_DAYS_PER_WEEK = 5;

类名

类名和对象名应该是名词或名词短语。

正确实例:如 Customer、WikiPage、 Account和AddressParser

错误实例:如 Manager、Processor

方法名

方法名应当是动词或动词短语。

正确实例:如 如postPayment、deletePage或 save。

函数

短小

函数不应该大到足以容纳嵌套结构。建议:函数的缩进层级不该多于一层或两层。这样的函数易于阅读和理解。

PS:根据个人经验,只要这个函数超过了30行。。。过一两个月,基本就没办法一眼就能看明白该函数里面做了什么。

1
2
3
4
5
6
def delete_service_instance(namespace, service_name, new_instance_name, deploy_func):
url = build_url(namespace, service_name, delete_template_url, instance_name=new_instance_name)

payload = {}
response = deploy_func(url, headers=common_headers, data=payload)
return response

只做一件事

尽量把逻辑相关的操作都抽象到一个函数内,这也比较容易满足只做一件事。(但事情又可大可小,所以这个看自己去把控了)

1
2
3
4
5
6
# 求给定数中的偶数
def compute_even_by_num(num):
return [x for x in range(0, num) if x % 2 == 0]

# 上述代码优化, 比如还需要对num进行参数校验,那么就应该抽象出一个通用的数值类型的参数校验。
# 实现某个业务中的指定功能。。。(大部分就是这种场景,容易把函数写的很大)

每个函数一个抽象层级

可以简单理解 :实现的细节以及步骤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 全部是步骤
private void includeSetupAndTeardownPages() throws Exception {
includeSetupPages();
includePageContent();
includeTeardownPages();
updatePageContent();
}

// 细节
private SetupTeardownIncluder(PageData pageData) {
this.pageData = pageData;
testPage = pageData.getWikiPage();
pageCrawler = testPage.getPageCrawler();
newPageContent = new StringBuffer();
}

别重复自己

抽离出Try/Catch代码

正确实例

1
2
3
4
5
6
7
public void delete(Page page) { 
try {
deletePageAndAllReferences(page);
}catch (Exception e) {
logError(e);
}
}

注释

注释不能美化糟糕的代码, 写注释的常见动机之一是糟糕的代码的存在。

我们编写一个模块,发现它令人困扰、乱七八糟。我们知道,它烂透了。我们告诉自己:“喔,最好写点注释!”。。不然过段时间都不知道这函数是在干嘛。。。😂

好注释

  • 提供信息的注释,返回某个方法具体的返回值类型以及大致数据格式;
  • 对意图的解释;
  • 警示;
    1
    2
    // Don't run unless you 
    // have some time to kill.
  • TODO注释;
  • 公共API或者方法中的Javadoc;

坏的注释

  • 多余的注释,把方法实现的逻辑解释了一遍;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // Utility method that returns when this.closed is true. Throws an exception 
    // if the timeout is reached.
    public synchronized void waitForClose(final long timeoutMillis) throws Exception {
    if(!closed) {
    wait(timeoutMillis);
    if(!closed)
    throw new Exception("MockResponseSender could not be closed");
    }
    }
  • 误导性注释,很多时候表达的意思可能不是代码中真正实现的意思,或者由于很久未维护导致注释与真实内容不一致;

  • 日志式注释;

    1
    2
    3
    4
    5
    * Changes (from 11-Oct-2001) 
    * --------------------------
    * 11-Oct-2001 : Re-organised the class and moved it to new package * com.jrefinery.date (DG);
    * 05-Nov-2001 : Added a getDescription() method, and eliminated NotableDate * class (DG);
    ...
  • 废话注释

    1
    2
    3
    4
    5
    6
    7
    8
    /** The day of the month. */ 
    private int dayOfMonth;

    /** * Returns the day of the month.
    ** @return the day of the month. */
    public int getDayOfMonth() {
    return dayOfMonth;
    }
  • 长期注释掉的代码

  • 信息过多 or 不明显的联系
    过滤器字节是什么?与那个+1 有关系吗?或与*3 有关?还是与两 者皆有关?为什么用200?

    1
    2
    3
    /* * start with an array that is big enough to hold all the pixels 
    * (plus filter bytes), and an extra 200 bytes for header info */
    this.pngBytes = new byte[((this.width + 1) * this.height * 3) + 200];

测试

  • 避免测试不足;
  • 采用覆盖率工具;
  • 别掠过小测试;
  • 被忽略的测试就是对不确定事物的疑问;
  • 测试边界条件;
  • 全面测试相近的缺陷 ;
  • 测试失败的模式有启发性;
  • 测试应该快速 。

如何写出上面所述的代码呢?

​ 写代码和写别的东西很像。在写论文或文章时,你先想什么就写什么,然后再打磨它。初稿也许粗陋无序,你就斟酌推敲,直至达到你心目中的样子。

​ 写函数时,一开始都冗长而复杂。有太多缩进和嵌套循环。有过长的参数列表。名称是随意取的,也会有重复的代码。

​ 然后还需要做的:

  • 配上一套单元测试,覆盖每行丑陋的代码
  • 然后打磨这些代码,分解函数、修改名称、消除重复,同时保持测试通过;
------------- 本 文 结 束 感 谢 您 的 阅 读 -------------