Clean Code 閱讀總結

網易考拉前端團隊發表於2017-09-25

1 開始

本文是在閱讀 clean code 時的一些總結,原書是基於 Java 的,這裡將其中的一些個人認為實用性較強且容易與日常業務開發結合的一些原則重新進行整理,並參考了 clean-code-javascript 一文給出了一些程式碼例項,希望本文能夠給日常開發編碼和重構作出一些參考。

2 有意義的命名

2.1 名副其實

變數取名要花心思想想,不要貪圖方便,過於簡略的名稱,時間長了以後就難以讀懂。

// bad
var d = 10;
var oVal = 20;
var nVal = 100;


// good
var days = 10;
var oldValue = 20;
var newValue = 100;複製程式碼

2.2 避免誤導

命名不要讓人對變數的資訊 (型別,作用) 產生誤解。

accounts 和 accountList,除非 accountList 真的是一個 List 型別,否則 accounts 會比 accountList 更好。因此像 List,Map 這樣的字尾,不要隨意使用。

// bad
var platformList = {
    web: {},
    wap: {},
    app: {},
};


// good
var platforms = {
    web: {},
    wap: {},
    app: {},
};複製程式碼

2.3 做有意義的區分

用明確的意義去表述變數直接的區別。

很多情況下,會有存在 product,productData,productInfo 之類的命名,Data 和 Info 很多情況下並沒有明顯的區別,不如直接就使用 product。

// bad
var goodsInfo = {
    skuDataList: [],
};

function getGoods(){};          // 獲取商品列表
function getGoodsDetail(id){};  // 通過商品ID獲取單個商品


// good
var goods = {
    skus: [],
};

function getGoodsList(){};      // 獲取商品列表
function getGoodsById(id){};    // 通過商品ID獲取單個商品複製程式碼

2.4 使用讀得出來的名稱

縮寫要有個度,比如像 DAT 這樣的寫法,到底是 DATA 還是 DATE...

// bad
var yyyyMMddStr = eu.format(new Date(), 'yyyy-MM-dd');
var dat = null;
var dev = 'Android';


// good
var todaysDate = eu.format(new Date(), 'yyyy-MM-dd');
var data = null;
var device = 'Android';複製程式碼

2.5 使用可搜尋的名稱

可搜尋的名稱能夠幫助快速定位程式碼,尤其對於一些數字狀態碼,不建議直接使用數值,而是使用列舉。

// bad
var param = {
    periodType: 0,
};


// good
const HOUR = 0, DAY = 1;
var param = {
    periodType: HOUR,
};複製程式碼

2.6 避免使用成員字首

把類和函式做得足夠小,消除對成員字首的需要。因為長期以後,字首在人們眼裡會變得越來越不重要。

2.7 新增有意義的語境

對於某些名稱,在不同語境下可能代表不同的含義,最好為它新增有意義的語境。

firstName,lastName,street,houseNumber,city,state,zipcode 一連串變數放在一起可以判斷是一個地址,但是如果將這些變數單獨拎出來,有些變數名意義就不明確了。這時可以新增語境明確其意義,如 addrFirstName,addrLastName,addrState。

當然也不要隨意新增語境,這樣只會讓變數名變得冗長。

// bad
var firsName, lastName, city, zipcode, state;
var sku = {
    skuName: 'sku0',
    skuStorage: 'storage0',
    skuCost: '10',
};


// good
var addrFirsName, addrLastName, city, zipcode, addrState;
var sku = {
    name: 'sku0',
    storage: 'storage0',
    cost: '10',
};複製程式碼

2.8 變數名從一而終

變數名取名多花一點時間,如果這一物件會在多個函式,模組中使用,就應該使用一致的變數名,否則每次看到這個物件,都需要重新去理清變數名,造成閱讀障礙。

// bad
function searchGoods(searchText) {
    getList({
        keyword: searchText,
    });
}
function getList(option) {

}

// good
function searchGoods(keyword) {
    getList({
        keyword: keyword,
    });
}

function getList(keyword) {}複製程式碼

3 函式

3.1 短小

短小是函式的第一規則,過長的函式不僅會造成閱讀困難,在維護的時候難度也會增加。短小,要求每個函式做盡可能少的事情,同時減少程式碼的巢狀和縮排,要知道,程式碼的巢狀和縮減同樣會帶來閱讀的困難。

// bad
function initPage(initParams) {
    var data = this.data;
    if ('dimension' in initParams) {
        data.dimension = initParams.dimension;
        data.tab.source.some(function(item, index){
            if (item.value === data.dimension) {
                data.tab.defaultIndex = index;
            }
        });
    }
    if ('standardMedium' in initParams) {
        data.hasStandardMedium = true;
        data.filterParams[data.dimension].standardMedium = initParams.standardMedium;
    }
    if ('plan' in initParams || 'name' in initParams) {
        data.filterParams[data.dimension].planQueryString = initParams.plan || initParams.name;
    } else if ('traceId' in initParams) {
        data.filterParams[data.dimension].planQueryString = 'id:' + initParams.traceId;
    }
}

// good
function initPage(initParams) {
    initDimension(initParams);
    initStandardMedium(initParams);
    initPlanQueryString(initParams);
}
function initDimension(initParams) {
    var data = this.data;
    if ('dimension' in initParams) {
        data.dimension = initParams.dimension;
        data.tab.source.some(function(item, index){
            if (item.value === data.dimension) {
                data.tab.defaultIndex = index;
            }
        });
    }
}
function initStandardMedium(initParams) {
    var data = this.data;
    if ('standardMedium' in initParams) {
        data.hasStandardMedium = true;
        data.filterParams[data.dimension].standardMedium = initParams.standardMedium;
    }
}
function initPlanQueryString() {
    var data = this.data;
    if ('plan' in initParams || 'name' in initParams) {
        data.filterParams[data.dimension].planQueryString = initParams.plan || initParams.name;
    } else if ('traceId' in initParams) {
        data.filterParams[data.dimension].planQueryString = 'id:' + initParams.traceId;
    }
}複製程式碼

3.2 只做一件事情

函式應該做一件事情,做好這件事,只做這一件事。

如果函式只是做了該函式名下同一個抽象層上的步驟,則函式還是隻做了一件事。當函式中出現另一抽象層級所做的事情時,則可以將這部分拆成另一層級的函式,因此縮小函式。

當一個函式可以被劃分成多個區段時(程式碼塊)時,這就說明了這個函式做了太多事情。

// bad
function onTimepickerChange(type, e) {
    if(type === 'base') {
        // do base type logic...
    } else if (type === 'compare') {
        // do compare type logic...
    }
    // do other stuff...
}

// good
function onBaseTimepickerChange(e) {
    // do base type logic
    this.doOtherStuff();
}

function onCompareTimepickerChange(e) {
    // do compare type logic
    this.doOtherStuff();
}

function doOtherStuff(){}複製程式碼

3.3 每個函式一個抽象層級

一個函式中不應該混雜了多個抽象層級,即同一級別的步驟才放到一個函式中,因為通過這些步驟就能完整地完成一件事情。

回到之前提到變數命名的問題,一個變數或函式,其作用域餘越廣,就越需要一個有意義的名字來對其進行描述,提高可讀性,減少在閱讀程式碼時還需要去查詢定義程式碼的頻率,有些時候有意義的名字就可能需要更多的字元,但這是值得的。但對於小範圍使用的變數和函式,可以適當縮短名稱。因為過長的名稱,某些時候反而會增加閱讀的困難。

可以通過向下原則劃分抽象層級

程式就像是一系列 TO 起頭的段落,每一段都描述當前層級,並引用位於下一抽象層級的後續 TO 起頭段落
- 如果要完成 A,需要完成 B,完成 C;
- 要完成 B,需要完成 D;
- 要完成 C,需要完成 E;複製程式碼

函式名明確了其作用,獲取一個圖表和列表,函式中各個模組的邏輯進行了劃分,明確各個函式的分工, 拆分的函式名直接表明了每個步驟的作用, 不需要額外的註釋和劃分。在維護的時候, 可以快速的定位各個步驟, 而不需要在一個長篇幅的函式中需找對應的程式碼邏輯.

實際業務例子, 資料門戶-流量看板-流量總覽的一個獲取趨勢圖和右邊列表的例子。選擇一個通過 tab 選擇不同的指標,不同的指標影響的趨勢圖和右邊列表的內容,兩個模組的資料合併到一個請求中得到。流水賬的寫法可以將函式寫成下面的樣子,這種寫法有幾個明顯的缺點:

  • 長。通常情況下趨勢圖配置可能就需要20多行,整個函式加起來,輕易就超過50行了;
  • 函式名不準確。函式名僅表明是獲取一個圖表的,但實際上還獲取了右邊列表資料並進行了配置;
  • 函式層級混亂,還可以進行更細的劃分;

根據向下原則

// bad
getChart: function(){
    var data = this.data;
    var option = {
        url: '/chartUrl',
        param: {
            dimension: data.dimension,
            period: data.period,
            comparePeriod: data.comparePeriod,
            periodType: data.periodType,
        },
        fn: function(json){
            var data = this.data;
            // 設定圖表
            data.chart = json.data.chart;
            data.chart.config = {
                //... 大量的圖表配置,可能有20多行
            }
            // 設定右邊列表
            data.sideList = json.data.list;
        }
    };
    // 獲取請求引數
    this.fetchData(option);
},

// good
getChartAndSideList: function(){
    var option = {
        url: '/chartUrl',
        param: this.getChartAndSideListParam();
        fn: function(json){
            this.setChart(json);
            this.setSideList(json);
        }
    };
    this.fetchData(option);
},複製程式碼

3.4 switch語句

switch語句會讓程式碼變得很長,因為switch語句天生就是要做多件事情,當狀態不斷增加的時候,switch語句也會不斷增加。因此可能把取代switch語句,或者將其放在較低的層級.

放在底層的意思,可以理解為將其埋藏到抽象工廠地下,利用抽象工廠返回內涵不同的方法或物件來進行處理.

3.5 減少函式的引數

函式的引數越多,不僅註釋寫得長,使用的時候容易使得函式引數發生錯位。當函式引數過多時,可以考慮以引數列表或者物件的形式傳入.

資料門戶裡面的一個例子:

// bad
function getSum(a [, b, c, d, e ...]){}


// good
function getSum(arr){}複製程式碼
// bad
function exportExcel(url, param, onsuccess, onerror){}


// good
/**
 * @param option
 *    @property url
 *    @property param
 *    @property onsucces
 *    @property onerror
 */
function exportExcel(option){}複製程式碼

引數儘量少,最好不要超過 3 個

3.6 取個好名字

函式應該取個好一點的名字,適當使用動詞和關鍵字可以提高函式的可讀性。例如:

一個判斷是否在某個區間範圍的函式,取名為 within,從名稱上可以容易判斷出函式的作用,但是這仍然不是最好的,因為這個函式帶有三個引數,無法一眼看出這個函式三個引數之間的關係,是 b <= a && a<= c,還是 a <= b && b <= c ?

或許可以通過更改引數名來表達三個引數的關係,這個必須看到函式的定義後才可能得知函式的用法.

如果再把名字改一下,從名字就可以容易得知三個引數依次的關係,當然這個名字可能會很長,但如果這個函式需要大範圍地使用,較長的名字換來更好的可讀性,這一代價是值得的.

// bad
function within(a, b, c){}

// good
function assertWithin(val, min, max){}

// good
function assertValWithinMinAndMax(val, min, max){}複製程式碼

3.7 無副作用

一個有副作用的函式,通常都是是非純函式,這意味著函式做的事情其實不止一件,函式所產生的副作用被隱藏了,函式呼叫者無法直接通過函式名來明確函式所做的事請.

4 註釋

4.1 好註釋

法律資訊,提供資訊的註釋,對意圖的解釋,闡釋,警示,TODO,放大(放大某種看似不合理程式碼的重要性),公共 API 註釋

儘量讓函式,變數變得刻度,不要依賴註釋來描述,對於複雜難懂的部分才適當用註釋說明.

4.2 壞註釋

喃喃自語,多餘的註釋(例如本來函式名就能夠說明意圖,還要加註釋),誤導性註釋,循規式註釋(為了規範去加註釋,其實函式名和引數名已經可以明確資訊了),日誌式註釋(記錄無用修改日誌的註釋),廢話註釋

4.3 原則

  1. 能用函式或變數說明時,就別用註釋,這就意味著要花點時間取個好名字
// bad
var d = 10;     // 天數

// good
var days = 10;複製程式碼
  1. 註釋掉的程式碼不要留,重要的程式碼是不會被註釋掉的

資料門戶-實時概況裡面的一段程式碼,/src/javascript/realTimeOverview/components/index.js

// bad
function dimensionChanged(dimension){
    var data = this.data.keyDealComposition;
    data.selectedDimension = dimension;
    // 2016.10.31 modify:產品改動,選擇品牌分佈的時候不顯示二級類目
    // if (dimension.dimensionId == '6') {
    //     data.columns[0][0].name = dimension.dimensionName;
    //     data.columns[0].splice(1, 0, {name:'二級類目', value:'secCategoryName', noSort: true});
    // } else {
        this.handle('util.setTableHeader');
    // }
    this.handle('refreshComposition');
};

// good
function dimensionChanged(dimension){
    var data = this.data.keyDealComposition;
    data.selectedDimension = dimension;
    this.handle('util.setTableHeader');
    this.handle('refreshComposition');
};複製程式碼
  1. 不要在註釋裡面加入太多資訊,沒人會看

  2. 非公用函式,沒有必要加過多的註釋說明,冗餘的註釋會使程式碼變得不夠緊湊,增加閱讀障礙

// bad
/**
 * 設定表格表頭
 */
function setTableHeader(){},

// good
function setTableHeader(){},複製程式碼
  1. 括號後的註釋
// bad
function doSomthing(){
    while(!buffer.isEmpty()) {  // while 1
        // ...
        while(arr.length > 0) {  // while 2
            // ...
            if() {

            }
        } // while 2
    } // while 1
}複製程式碼
  1. 不需要日誌式,歸屬式註釋,相信版本控制系統
// bad
/**
 * 2016.12.03 bugfix, by xxxx
 * 2016.11.01 new feature, by xxxx
 * 2016.09.12 new feature, by xxxx
 * ...
 */


// bad
/**
 * created by xxxx
 * modified by xxxx
 */
function addSum() {}

/**
 * created by xxxx
 */
function getAverage() {
    // modified by xxx
}複製程式碼
  1. 儘量別用用位置標記
// bad

/*************** Filters ****************/

///////////// Initiation /////////////////複製程式碼

5 格式

5.1 垂直方向

  1. 相關程式碼緊湊顯示,不同部分的用空格隔開
// bad
function init(){
    this.data.chartView = this.$refs.chartView;
    this.$parent.$on('inject', function () {
        this.dataConvert(this.data.source);
        this.draw();
    });
    this.$watch('source', function (newValue, oldValue) {
        if (newValue && newValue != this.data.initValue) {
            this.dataConvert(newValue);
            this.draw();
        } else if (!newValue) {
            if (self.data.chartView) {
                this.data.chartView.innerHTML = '';
            }
        }
    }, true);
}

// good
function init(){
    this.data.chartView = this.$refs.chartView;

    this.$parent.$on('inject', function () {
        this.dataConvert(this.data.source);
        this.draw();
    });

    this.$watch('source', function (newValue, oldValue) {
        if (newValue && newValue != this.data.initValue) {
            this.dataConvert(newValue);
            this.draw();
        } else if (!newValue) {
            if (this.data.chartView) {
                this.data.chartView.innerHTML = '';
            }
        }
    }, true);
}複製程式碼
  1. 不要在程式碼中加入太多過長的註釋,阻礙程式碼閱讀
// bad
BaseComponent.extend({
    checkAll: function(status){
        status = !!status;
        var data = this.data;
        this.checkAllList(status);
        this.checkSigList(status);
        data.checked.list = [];
        if(status){
            // 當全選的時候先清空列表, 然後在利用Array.push新增選中項
            // 如果在全選的時候不能直接checked.list = dataList
            // 因為這樣的話後面對checked.list的操作就相當於對dataList直接進行操作
            // 利用push可以解決這一個問題
            data.sigList.forEach(function(item,i){
                data.checked.list.push(item.data.item);
            })
        }
        this.$emit('check', {
            sender: this,
            index: CHECK_ALL,
            checked: status,
        });
    },
});

// good
BaseComponent.extend({
    checkAll: function(status){
        status = !!status;
        this.checkAllList(status);
        this.checkSigList(status);
        this.clearCheckedList();
        if(status){
            this.updateCheckedList();
        }

        this.emitCheckEvent(CHECK_ALL, status);
    },
});複製程式碼
  1. 函式按照依賴順序佈局,被呼叫函式應該緊跟呼叫函式
// bad
function updateModule() {}
function updateFilter() {}
function reset() {}
function refresh() {
    updateFilter();
    updateModule();
}

// good
function refresh() {
    updateFilter();
    updateModule();
}
function updateFilter() {}
function updateModule() {}
function reset() {}複製程式碼
  1. 相關的,相似的函式放在一起
// bad
function onSubmit() {}
function refresh() {}
function onFilterChange() {}
function reset() {}

// good
function onSubmit() {}
function onFilterChange() {}

function refresh() {}
function reset() {}複製程式碼
  1. 變數宣告靠近其使用位置
// bad
function (x){
    var a = 10, b = 100;
    var c, d;

    a = (a-b) * x;
    b = (a-b) / x;
    c = a + b;
    d = c - x;
}

// good
function (x){
    var a = 10, b = 100;

    a = (a-b) * x;
    b = (a-b) / x;

    var c = a + b;
    var d = c - x;
}複製程式碼

5.2 水平方向

  1. 運算子號之間空格,但是要注意運算優先順序
// bad
var v = a + (b + c) / d + e * f;

// good
var v = a + (b+c)/d + e*f;複製程式碼
  1. 變數水平對齊意義不大,應該讓其靠近
// bad
var a       = 1;
var sku     = goodsInfo.sku;
var goodsId = goodsInfo.goodsId;

// good
var a = 1;
var sku = goodsInfo.sku;
var goodsId = goodsInfo.goodsId;複製程式碼

5.4 對於短小的if,while語句,也要儘量保持縮排

突然間改變縮排的規律,很容易就會被閱讀習慣欺騙

// bad
if(empty){return;}


// good
if(empty){
    return;
}

// bad
while(cli.readCommand() != -1);
app.run();


// good
while(cli.readCommand() != -1)
;

app.run();複製程式碼

6 實際業務程式碼中的應用

龐大的config函式

對於一些較為複雜的元件或頁面元件,需要定義很多屬性,同時又要對這部分屬性進行初始化和監聽,像下面這段程式碼。在好幾個大型的頁面裡面都看到了類似的程式碼,config 方法少的有 100行,多的有 400行。

config 方法基本就是一個元件的入口,在進行維護的時候一般都會先讀 config 方法,但是對於這麼長的函式,很容易第一眼就懵了。

Component.extend({
    template: tpl,
    config: function(data){
        eu.extend(data, {
            tabChartTab: 0,
            periodType: 0,
            dimensionType: 1,
            dealConstituteCompare:false,
            dealConstituteSort: {
                dimensionValue: 'sales',
                sortType: 0,
            },
            dealConstituteDecorate: {
                noCompare:[],
                progress: ['salesPercent'],
                sort:[
                ]
            },
            defaultMetrics: [
            ],
            // ...下面還有幾百行關於其他模組的屬性, flow, hotSellRank等
        });

        this.$watch('periodType', function(){
            // ...
        });

        this.$watch('topCategoryId', function(){
            // ...
        });

        // 這裡還有一部分非同步請求程式碼...
        this.refresh();
    },
})複製程式碼

針對上述這段程式碼程式碼,明顯的缺點是:

  • 太長
  • 變數命名有冗餘資訊,且搜尋性差
  • 變數(屬性)太多
  • 做的事情太多,初始化元件屬性,新增監聽方法,還有一些業務邏輯程式碼

這對這些可以作出一些改進:

  • 使用列舉代替數值
  • config內只保留一切作為範圍加大屬性的直接初始化程式碼,其餘針對於模組的屬性將通過呼叫 initData 方法來初始化
  • initData 進一步根據模組劃分初始化方法
  • 對於屬於摸個模組的屬性,則將其劃分到同一個物件上,減少元件上掛載的屬性數量,同時也簡化了屬性的命名
  • 監聽方法同樣是通過 addWatchers 初始化
  • 初始化過程中需要執行的部分邏輯,儘可能放在 init 等元件例項化後執行
const TAB_A = 0, TAB_B = 1;
const HOUR = 0, DAY = 1;
const DIMENSION_A = 0, DIMENSION_B = 1;
const DISABLE = false, ENABLE = true;

Component.extend({
    template: tpl,
    config: function(data){
        eu.extend(data, {
            tabChartTab: TAB_A,
            periodType: HOUR,
            dimensionType: DIMENSION_B,
        });

        this.initData();
        this.addWatchers();
    },

    initData: function(){
        this.initDealConsitiuteData();
        this.initFlowData();
        this.initHotSellRank();
    },

    initDealConsitiuteData: function(){
        this.data.dealConstitute = {
            compare: DISABLE,
            sort: {
                dimensionValue: 'sales',
                sortType: 0,
            },
            decorate: {
                noCompare:[],
                progress: ['salesPercent'],
                sort:[
                ]
            },
            defaultMetrics: [
            ],
        }
    },

    addWatchers: function(){
        this.$watch('periodType', function(){
            // ...
        });

        this.$watch('topCategoryId', function(){
            // ...
        });
    },

    init: function(){
        // 部分初始化要執行的邏輯
        this.refresh();
    },

})複製程式碼

其實按照上面進行優化以後,程式碼的可讀性是有所提高,但由於這是一個頁面元件,程式碼行數極多,修改後方法變得更多了,仍然不便於閱讀。所以,針對於這種大型的頁面,更適當的做法是,將頁面拆分為幾個模組,將業務邏輯拆分,減少每個模組的程式碼量,提高可讀性。而對於不可再拆分的元件或模組,如果仍然包含大量需要初始化的屬性,上述例子就可以作為參考了。

7 總結

本文整理的幾個要點:

  • 寫程式碼就像寫故事,裡面各個角色 (變數,函式) 的名字要取得好,才讀得流暢;
  • 函式要短小,不要混雜太多不相關,不同層級的邏輯;
  • 註釋要精簡準確,能不寫就不要寫;
  • 程式碼佈局要向報紙學習,排版注意垂直與水平方向的間隔,聯絡緊密的佈局要緊湊;

就算是經驗老道的大神,也很難一遍就能寫出簡潔的程式碼,所以要勤於對程式碼進行重構,邊寫程式碼邊修改。程式碼只有在經過一遍一遍修改和錘鍊以後,才會逐漸地變得簡潔和精緻。

8 參考

  1. Clean Code
  2. clean-code-javascript

相關文章