代码质量管理——如何写出优雅地代码

原文:https://segmentfault.com/a/1190000013397521


作为一个刚写代码不久的小菜鸟,工作的半年多让我越发意识到提高代码质量的重要性。从前只会关注实现功能,慢慢的开始关注性能,现阶段则发现其实还有很多细节也是(如可读性、易用性、可维护性、一致性)提高代码质量的关键。“实现功能”跟“优雅地实现功能”是两码事。

 :大部分内容归纳自网络,将多篇文章的观点汇总加工了一下,也融合了一些个人的见解。

原则

  • 单一职责原则

  • 易用性原则

  • 可读性原则

  • 复杂性守恒原则:无论你怎么写代码,复杂性都是不会消失的

注:如果逻辑很复杂,那么代码看起来就应该是复杂的。如果逻辑很简单,代码看起来就应该是简单的。

单一职责原则

面向对象五大设计模式基本原则之一。即一部分代码只应该用于某一个特定功能,不应与其他功能耦合在一起

假设你的一个function同时实现了功能a和功能b,之后需求变更,你需要修改功能a,但是因为这两个功能都在一个function里,你就不得不再去确认是否会影响到功能b。这就造成了不必要的成本。

如下我总结了三个拆分代码的原则:

“and”原则

当你为你的方法命名时不得不加上“and”时,就该考虑考虑是不是要把这个方法拆分一下了。

“100”原则

当你的一个function超过一百行时,一定要进行拆分了。

注:这里的100可能有点多,只是对我个人而言,100算是我的极限,总之就是绝对不要将一个函数写的太长。

命令、查询拆分原则

我们开发中大部分操作可以总结为“命令”和“查询”,如写cookie、修改data、发送post请求都可以叫“命令”,而读取cookie、ajax获取数据则认为是“查询”操作。

函数式编程中讲究“数据不可变”,即:

只有纯的没有副作用的函数,才是合格的函数。

副作用:指当调用函数时,除了返回函数值之外,还对主调用函数产生附加的影响。例如修改全局变量(函数外的变量)或修改参数。

好处是使得开发更加简单,可回溯,测试友好,减少了任何可能的副作用。

将“命令”与“查询”拆分实际上就是函数式编程思想的部分体现,参考如下代码:


  1. function getFirstName() {

  2.  var firstName = document.querySelector("#firstName").value;

  3.  firstName = firstName.toLowerCase();

  4.  setCookie("firstName", firstName);

  5.  if (firstName === null) {

  6.      return "";

  7.  }

  8.  return firstName;

  9. }

  10. var activeFirstName = getFirstName();

通过名字来看,该方法是用于获取first name的,但实际上它还设置了cookie,这是我们没有预料的。对于一个“查询”方法,它不应该有任何修改方法外变量的行为,即“副作用”。更好的写法如下:


  1. function getFirstName() {

  2.  var firstName = document.querySelector("#firstName").value

  3.  if (firstName === null) {

  4.      return "";

  5.  }

  6.  return firstName;

  7. }

  8. setCookie("firstName", getFirstName().toLowerCase());

一目了然,getFirstName只返回firstName,而设置cookie操作在它之外进行。

易用性原则

简单

这里的简单,主要归结为function的一些设计原则,有如下几点:调用简单、易理解、减少记忆成本、参数处理。

如下,仅仅想实现修改dom颜色、宽度等属性,原生代码如下:


  1. document.querySelector('#id').style.color = 'red'

  2. document.querySelector('#id').style.width = '123px'

  3. document.querySelector('#id').style.height = '456px'

而封装过后:


  1. function a(selector, color, width, height) {

  2. document.querySelector(selector).style.color = color

  3. document.querySelector(selector).style.width = width

  4. document.querySelector(selector).style.height = height

  5. }

  6. a('#a', 'red')

瞬间变得简单可用了 ~

但该方法还存在一个问题,那就是命名太抽象了。。。除了开发者自己以外不可能有人在不看源码的情况下一眼看出这个方法a是干嘛的。那么咱再把这个方法名改写得更易理解一点:


  1. function letSomeElementChange(selector, color, width, height) {

  2. document.querySelector(selector).style.color = color

  3. document.querySelector(selector).style.width = width

  4. document.querySelector(selector).style.height = height

  5. }

这样我们就能一目了然该方法的作用 ~ 不过仍有可优化的地方。这么长的方法名谁记得住,要减少记忆成本啊,再改个名:


  1. function setElement(selector, color, width, height) {

  2. document.querySelector(selector).style.color = color

  3. document.querySelector(selector).style.width = width

  4. document.querySelector(selector).style.height = height

  5. }

OK,目前这个方法已经满足它的职责并且很好用了,但还觉得怪怪的。这一坨参数太碍眼。。。


  1. function setElement(selector, opt) {

  2. const { color, width, height } = opt

  3. color && document.querySelector(selector).style.color = color

  4. width && document.querySelector(selector).style.width = width

  5. height && document.querySelector(selector).style.height = height

  6. }

把多个参数合并一下,并在内部做兼容处理,这个方法便易用多了。即使不传第二个参数也不会有任何副作用。

一致性

假如有这样一个方法,获取歌曲列表,并将其设置到div的innerText中:


  1. function getSongs() {

  2. return $.get('/songs).then((response) {

  3.    div.innerText = response.songs

  4. })

  5. }

这就违背了方法的表里一致性,也违背了上文的单一职责原则命令、查询拆分原则,因为它不仅获取了歌单,同时还修改了innerText,要让其更合理:

低耦合

耦合是衡量一个程序单元对其他程序单元的依赖程度。耦合(或高耦合)是应该极力避免的。如果你发现自己正在复制和粘贴代码并进行小的更改,或者重写代码,因为其他地方发生了更改,这就是高耦合的体现。

耦合会严重影响代码的复用性及可扩展性,让后人维护时不得不修改甚至重写这部分代码,不仅浪费时间还会导致仓储中又多出一块类似的代码,很容易让人迷惑。

同时,修改耦合度高的代码时经常会牵一发而动全身,如果修改时没有理清这些耦合关系,那么带来的后果可能会是灾难性的,特别是对于需求变化较多以及多人协作开发维护的项目,修改一个地方会引起本来已经运行稳定的模块错误,严重时会导致恶性循环,问题永远改不完,开发和测试都在各种问题之间奔波劳累,最后导致项目延期,用户满意度降低,成本也增加了,这对用户和开发商影响都是很恶劣的,各种风险也就不言而喻了。

高内聚

不应该将没有任何联系的东西堆到一起。

内聚是一个类中变量与方法连接强度的尺度。高内聚是值得要的,因为它意味着类可以更好地执行一项工作。低内聚是不好的,因为它表明类中的元素之间很少相关。每个方法也应该高内聚,大多数的方法只执行一个功能,不要在方法中添加‘额外’的指令,这样会导致方法执行更多的函数,同时也违反了上文的单一职责原则

低内聚的体现:如果属性没有被类中的多个方法使用,这可能是低内聚的标志。同样,如果方法在几种不同的情况下不能被重用,或者如果一个方法根本不被使用,这也可能是低内聚的一个标志。

高内聚有助于缓解高耦合,高耦合是需要高内聚的标志。但是,如果两个问题同时存在,应当选择内聚的方式。对于开发者来说,高内聚通常比低耦合更有帮助,尽管两者通常可以一起完成。

错误处理

  • 可预见的错误:诸如ajax回调、函数参数,这类问题很好解决,只需在开发时多考虑一步,对各种极端情况做好兼容即可。

  • 不可预见的错误:类似兼容性问题,这类问题无法在开发时准确预见的错误,可以准备好抛错,console.error/log/warn,最后你还可以为自己的程序留些后路: try...catch。

可读性原则

命名

命名应该保证别人通过名称一眼就能知道这个变量保存的是什么,或者这个方法是用来做什么的。

普通变量、属性用名词如下:


  1. var person = {

  2.  name: 'Frank'

  3. }

  4. var student = {

  5.  grade: 3,

  6.  class: 2

  7. }

bool变量、属性用(形容词)或者(be动词)或者(情态动词)或者(hasX),如下:


  1. var person = {

  2.  dead: false, // 如果是形容词,前面就没必要加 is,比如isDead 就很废话

  3.  canSpeak: true, //情态动词有 can、should、will、need 等,情态动词后面接动词

  4.  isVip: true, // be 动词有 is、was 等,后面一般接名词

  5.  hasChildren: true, // has 加名词

  6. }

普通函数、方法用(动词)开头:


  1. var person = {

  2.  run(){}, // 不及物动词

  3.  drinkWater(){}, // 及物动词

  4.  eat(foo){}, // 及物动词加参数(参数是名词)

  5. }

回调、钩子函数:


  1. var person = {

  2.  beforeDie(){},

  3.  afterDie(){},

  4.  // 或者

  5.  willDie(){}

  6.  dead(){} // 这里跟 bool 冲突,你只要不同时暴露 bool dead 和函数 dead 就行,怕冲突就用上面的 afterDie

  7. }

  8. button.addEventListener('click', onButtonClick)

  9. var component = {

  10.  beforeCreate(){},

  11.  created(){},

  12.  beforeMount(){}

  13. }

命名一致性

  • 顺序一致性:比如 updateContainerWidth 和 updateHeightOfContainer 的顺序就令人很别扭

  • 时间一致性:有可能随着代码的变迁,一个变量的含义已经不同于它一开始的含义了,这个时候你需要及时改掉这个变量的名字。

这一条是最难做到的,因为写代码容易,改代码难。如果这个代码组织得不好,很可能会出现牵一发而动全身的情况(如全局变量就很难改)

注释

不需要多花哨,只要把作用、用法描述清楚即可。方法的标准注释应该如下:


  1. /**

  2. * [function_name description]

  3. * @param  {[type]} argument [description]

  4. * @return {[type]}          [description]

  5. */

  6. function function_name(argument) {

  7. // body...

  8. }

将方法的参数与返回值都写清楚,我目前用的IDE是sublime,使用Docblockr插件可以自动生成格式化注释,很方便。

Bad Smell

项目中我们经常能够遇这类代码,它们仍可用,但是很“臭”,国外管这类代码有一个统称,即“bad smell”。如下这类代码可以说是很“臭”了:

  • 表里不一的代码

  • 过时的注释

  • 逻辑很简单,但是看起来很复杂的代码

  • 重复的代码

  • 相似的代码

  • 总是一起出现的代码

  • 未使用的依赖

  • 不同风格的代码

样式规范

  1. 正确命名:class必须用“-”写法,不要用驼峰和下划线。

  2. 正确嵌套:正常情况下一定要将class嵌套闭合,否则就相当于添加到全局,如果有重复命名的class就会受影响。

  3. 拒绝copy:如果想复用已有的样式,直接在原有class上用“,”语法分割,就能应用,不要再copy一份样式,会让两份样式都被应用,就要考虑样式覆盖的问题,很不友好。

  4. 滥用class:没有必要加的class不要加,每个class的添加都应该有明确理由。滥用class的话可能会导致样式覆盖,不该应用这个样式的地方用了这个样式。

  5. 慎用 !important,会强行覆盖所有同属性样式,一旦使用后会让代码难以维护,开发过程中绝对不要依赖该方法。如下总结了一些使用 !important的经验:

  • 一定要优化考虑使用样式规则的优先级来解决问题而不是 !important

  • 只有在需要覆盖全站或外部 css(例如引用的 ExtJs 或者 YUI )的特定页面中使用 !important

  • 解决紧急线上问题可以使用,但之后也要尽快用可维护的方式将代码替换回来

  • 永远不要在全站范围的 css 上使用 !important

  • 永远不要在你的插件中使用 !important

说得容易,做起来难

破窗效应

此理论认为环境中的不良现象如果被放任存在,会诱使人们仿效,甚至变本加厉。一幢有少许破窗的建筑为例,如果那些窗不被修理好,可能将会有破坏者破坏更多的窗户。最终他们甚至会闯入建筑内,如果发现无人居住,也许就在那里定居或者纵火。一面墙,如果出现一些涂鸦没有被清洗掉,很快的,墙上就布满了乱七八糟、不堪入目的东西;一条人行道有些许纸屑,不久后就会有更多垃圾,最终人们会视若理所当然地将垃圾顺手丢弃在地上。这个现象,就是犯罪心理学中的破窗效应,在编程领域同样存在。

要做到:只要是经过你手的代码,都会比之前好一点。


END

代码质量管理——如何写出优雅地代码