Clean Code 閱讀總結

1 開始

本文是在閱讀 clean code 時的一些總結,原書是基於 Java 的,這裏將其中的一些我的認爲實用性較強且容易與平常業務開發結合的一些原則從新進行整理,並參考了 clean-code-javascript 一文給出了一些代碼實例,但願本文可以給平常開發編碼和重構做出一些參考。javascript

2 有意義的命名

2.1 名副其實

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

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


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

2.2 避免誤導

命名不要讓人對變量的信息 (類型,做用) 產生誤解。git

accounts 和 accountList,除非 accountList 真的是一個 List 類型,不然 accounts 會比 accountList 更好。所以像 List,Map 這樣的後綴,不要隨意使用。github

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


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

2.3 作有意義的區分

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

不少狀況下,會有存在 product,productData,productInfo 之類的命名,Data 和 Info 不少狀況下並無明顯的區別,不如直接就使用 product。json

// 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...bash

// 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 使用可搜索的名稱

可搜索的名稱可以幫助快速定位代碼,尤爲對於一些數字狀態碼,不建議直接使用數值,而是使用枚舉。app

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


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

2.6 避免使用成員前綴

把類和函數作得足夠小,消除對成員前綴的須要。由於長期之後,前綴在人們眼裏會變得愈來愈不重要。異步

2.7 添加有意義的語境

對於某些名稱,在不一樣語境下可能表明不一樣的含義,最好爲它添加有意義的語境。ide

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
相關文章
相關標籤/搜索