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
相关文章
相关标签/搜索