3.1 代码风格与破窗理论

在2019年小米的一次发布会上,雷军因为一句“我没有写过诗,但有人说我写过的代码像诗一样优雅”登上热搜,后来网上流传出一份代码的截图,从中可以看到顶部的版权信息、摘要注释、修订记录,最后才是工整的代码,即便没有学过汇编语言,相信大部分开发者都能感受到代码中表现出的严谨和认真。

不好的代码各有各的问题,而好的代码却存在很多共性:一致的语法风格、规范的命名方式、清晰的逻辑结构,简洁且优雅。对比一下我们小时候诵读的唐诗宋词,或五言或七言,或绝句或律诗,不就是对语法风格的统一要求吗?词牌名的使用也是一种命名规范,至于逻辑清晰和简洁优雅,诗词本身就已经是完美的诠释了。如果说代码是开发者的文字,那么无论是看似呆板的代码,还是优美动人的诗词,对美的表达是有异曲同工之妙的。

要想提高整个团队的代码质量,仅仅依靠制定编码规范是不够的,这就好像统治者除了依靠道德约束之外,还需要依赖法律的力量才能维持社会稳定一样。或许每个团队都有自己的开发规范,但几乎所有的团队也都需要面对代码维护的问题。我们最好能使用一些辅助工具,帮助开发者养成符合规范要求的良好的编码习惯,并在不符合项出现时给予提示,同时制定一些强制的拦截策略,以阻止不符合要求的代码入库。同时,你还需要通过多种途径,借鉴好的代码和设计模式来提高自己的编码水平。虽然用若干个嵌套在一起的for循环来实现某个业务逻辑并没有违背任何编码规范,但当你看到别人用map、filter、some等更具语义性的方法连在一起实现同一段业务逻辑时,立刻就会明白自己的代码语义不够清晰。你能够看懂高手使用的每一个方法,却无法写出类似的代码,很多时候这并不是代码本身的复杂性造成的,而仅仅是你还没有意识到原来还可以那样写。

很多时候初级开发者在开发中只是“用程序语言来翻译业务逻辑”,而并没有在进行“程序设计”,下面的示例或许能让你更好地理解这种差异。

假设有这样一个数组:

var dataList = [{
    id:0,
    name:'Tony',
    food:['apple', 'peach', 'coconut']
},{...},{...}];

现在需要将所有food字段的值用逗号连接起来,拼成一个字符串后发送给后台,你会如何编写代码呢?下面我们来看一些常见的写法:

/*版本一*/
var result = '';
for (var i = 0; i < dataList.length; i++){
    for (var j = 0; j < dataList[i].food.length; j++){
        result = result + dataList[i].food[j] + ',';
    }
}
result = result.slice(0,-1);
/*版本二*/
var result = [];
for (var i = 0; i < dataList.length; i++){
    for (var j = 0; j < dataList[i].food.length; j++){
        result.push(dataList[i].food[j]);
    }
}
result = result.join(',');
/*版本三*/
result = dataList.map(item=>item.food.join(',')).join(',');

对比上述三个版本,相信不需要多做解释,你也能明白笔者想要传达的东西。下面再来看一个例子,假设我们希望实现这样的功能:将学生的考试成绩转化为用ABCD表示的等级,如表3-1所示。

表3-1 分数区间对应等级表

038-1

下面先来看一些常见的写法(代码中省略了边界及类型检查):

/*版本一 直译业务逻辑*/
function transformScore(score) {
    if(score >= 60 && score < 70) return 'D';
    if(score >= 70 && score < 80) return 'C';
    if(score >= 80 && score < 90) return 'B';
    if(score >= 90 && score <= 100) return 'A';
}
/*版本二 使用对象+字典模式*/
function transformScore(score) {
    var resultMap = {
        '6':'D',
        '7':'C',
        '8':'B',
        '9':'A',
        '10':'A',
    }
    return resultMap[Math.floor(score / 10)];
}
/*版本三 使用字符串+字典模式*/
function transformScore(score) {
    return 'DCBAA'[(Math.floor(score / 10)) - 6];
}
/*版本四 使用ASCII码转换*/
function transformScore(score) {
    return score === 100 ? 'A' : String.fromCharCode(74 - Math.floor(score/10));
}

上述代码展示的四个版本中,版本三仅仅是转换结果较为简单时的一种特殊情况,相比之下,更推荐的做法是使用版本二的模式,尽管其看起来不如后两种版本简洁。也许有人会觉得版本四那样的代码很酷,但事实上它不仅增加了其他开发人员的理解难度,性能也不如其他几种版本。没有人会因为这样的代码而对你刮目相看,因为它怎么看都像是过分卖弄,好代码的首要条件必须是清晰且可读的。

犯罪心理学中著名的“破窗效应”并不仅仅适用于犯罪行为,符合这个法则的例子在生活中随处可见,对于软件工程来说也不例外。如果团队中定期组织开发人员进行代码检视,每个人都坚持依照规范来修正自己代码中不够好的部分,长此以往,整个工程文件就会保持在一个相对规范的状态,那么大家就可以把关注点聚焦在业务逻辑的复杂性和代码的健壮性上。一旦有人放松要求,开始贡献劣质代码,很快其他人就会降低对代码质量的要求,每个人都会觉得代码整体质量的恶化不是自己一个人造成的,不久之后整个项目的代码就会变得难以维护。很多时候,技术不得不为业务“开绿灯”,如果因为紧急情况来不及进行代码检视就发布上线,这时负责人最好让相关开发者在接下来的一周内完成代码检视和问题修复并主动公示,否则你将会发现越来越多的人以此为借口来绕开代码检视的环节。

拓展知识

破窗效应[1]是一个犯罪心理学理论,它认为如果放任环境中的不良现象存在,则会诱使人们效尤,甚至变本加厉。以一幢有少许破窗的建筑为例,如果那些窗户不及时修理好,可能就会引发破坏者故意破坏更多窗户。最终他们甚至还会闯入建筑物内,如果发现无人居住,也许就会在那里定居或纵火。再比如,如果一面墙上出现了一些涂鸦并且没有及时清洗掉,很快墙上就会布满乱七八糟、不堪入目的内容;如果一条人行道上有些许纸屑,不久之后就会有更多的垃圾,最终人们会理所应当地将垃圾顺手丢在地上。这些现象所表述的,就是犯罪心理学中的破窗效应。

在工程实践中你会发现,即使花费再多的精力去制定和完善编码规范,并不断组织学习和宣讲,也依然很难奢求此规范获得所有人的重视,因为在很多开发者的眼里,这属于“无关紧要的小事”。当一个人意识不到某些事情的重要性时,将事情的结果寄托在强制要求和自觉性上是一种不明智的做法。我们要做的是使用正确的工具来简化这一过程,这就好比是不断叮咛别人吃苹果与你把苹果洗干净递到别人手里的区别——“不想吃苹果”的意思其实只是“不想洗苹果”而已。前端领域已经提供了很多成熟的工具,我们只需要学习其用法,并将团队的开发规范融入进去即可,这样做的好处非常明显,具体说明如下。

  • 不同语言的开发者喜欢使用不同的开发工具,随着现代化开发中多语言混合开发的场景日益增多,配置文件能够帮助团队成员突破编辑器的限制而遵循相同的规则。
  • 能够节省代码评审的时间,我们可以安全地忽略与代码风格相关的所有问题,并专注于开发者真正需要关注的的事情,例如,代码的结构、语义、耦合度等。
  • 能够发现语法错误和一般类型错误、未定义的变量,等等,我们可以根据团队的实际情况启用不同的检查规则,事实上,ESLint官方公布的可开启和关闭的规则就多达200条。
  • 为工具设置配置文件几乎是一次性的工作,但其节省时间的作用却是持久化的。

下面就来学习如何利用各种辅助工具为团队制定编码规范。


[1]摘录自百度百科。