六个理念帮你写出好代码

代码首先是给写给人看的,就算不给别人看,自己也要能看的明白。

在我的编程生涯初期,我看过的编程书籍里,更偏向于《代码大全》、《代码整洁之道》这样的类型,反而具体的技术细节相关的书籍看的比较少。

这一章内容,我从编写好代码的底层逻辑来讲述。

区别于那些具体教你怎么样优化代码的文章,理解了这篇内容,你将会找到每个优化措施所对应的逻辑,自然而然的也会写出更好的代码。

童子军军规

我初次接触到这个理念是在《代码整洁之道》这本书中,整个理念只有一句话:

让营地比你来时更干净。

每次当你接手一段代码,开发完比你接手前更好,那么项目充斥的坏代码将会更少。

清理并不一定要花多少功夫,也许只是改好一个变量名,拆分一个有点过长的函数,消除一点点重复代码,清理一个嵌套 if 语句。

推崇这个理念,并不是让你时时刻刻都做到,牢记这么一个理念只会让你做事情越来越好,无论是否是编程领域。

六个简单的理念

对于如何编写代码,我遵循这么几个简单的理念。

1 清楚的命名

好的代码能够清楚的表述它所执行的逻辑,这也是所谓的“自解释代码”。

而自解释代码首先就需要你将代码中的变量、方法等声明清楚。

我不推崇一定要用英文进行命名,只要编程语言支持,你可以用任何你喜欢的“字符”来命名你的变量或方法。

但有一点请注意,请完整表述变量或方法所代表的含义

不要为了省事,而使用看不懂的缩写。

对于长命名,编辑器会有提示补全,你也可以通过鼠标双击选中变量并进行复制。


// bad
function change(bool) {
  value = bool;
}

function baDXFangBX() {
  kaiBX();
  fangDX();
  guanBX();
}

// good
function putElephantIntoFridge() {
  openFridge();
  putElephantIn();
  closeFridge();
}
​
function baDaXiangFangJinBingXiang() {
  daKaiBingXiang();
  fangJinDaXiang();
  guanBiBingXiang();
}
​
function 把大象放进冰箱() {
  打开冰箱();
  放进大象();
  关闭冰箱();
}
复制代码

清晰的语义让你看到任何一行代码,任何一个变量都能直观的知道这个东西是做什么的,而不需要你去找到变量的声明、或使用的地方,才能知道具体作用。

2 直接了当的语义化代码

虽然下面两段代码的含义相同,但是看到 forEach 就能知道是对数据里的每一个元素进行处理。

for (const item of array) {
  // do sth
}
​
array.forEach((item) => {
  // do sth
})
复制代码

你可以试着看看下面哪个代码更好理解。

// first
const target = array.find(item => item === 0)
if (target) {
  // do sth
}
​
// second
const hasTarget = array.map(item => item === 0 ? item : undefined).filter(item => item).length > 0
if (hasTarget) {
  // do sth
}
复制代码

第一段的意思是找到一个等于 0 的元素,如果有,做一些什么。

而第二段逻辑是找到数组中等于 0 的元素,过滤并判断数组长度,如果过滤后的数组长度大于 0(有等于 0 的元素),做一些什么。

虽然两段代码的功能是一样的,但是第二段的意思更加“绕口”一些。

3 模块化代码

模块化代码的含义是:将你的代码拆分成单独的“模块”,每个模块的代码拥有自己的含义,大到整个项目,小到一行代码,都可以作为单独的一个“模块”。

拆分方法

将多行语句拆分成方法,是常见的拆分方式。这使得多行代码的逻辑,能够用一句话来表达。

这是我从项目中截取的部分代码,最后四行分别做了设置标记、设置状态、触发默认选中事件,以及计算单元格列宽。使几百行代码在这一段中,浓缩为了“四句话”。

function dealRoomData(data) {
  // ... other codes
  
  this.setCombineFlag(selectedRooms, fields);
  this.setCombineStatus(fields);
  this.emitSelectionChange();
  this.calculateCellWidth([]);
}
复制代码

对齐代码

除了拆分成方法,合理划分代码也是一种将代码模块化的方法,使得在文件的某一块具备它自己的意思。

对于方法无论你使用以下哪种方式,都是可以的,主要还是看整体项目风格。

但是无论哪一种,都隐含着“模块”的理念在里面。

对于第一种,fuction 的首字母 f 是和 } 对齐的,在这整个一块区域里,就是 foo 的逻辑。

第二种也是,两个大括号之间的则是。(PS:js 会遇到自动补全分号导致的 bug,所以优先推荐第一种)

// first
function foo() {
  // other codes
}
​
// second
function foo()
{
  // other codes
}
复制代码

而对于标签,也是同样的道理,在一个闭合的区域里,包含的是同一功能的内容。

<component-name
  :disabled="disabled"
  class="some-classes"
  :value="value"
  @change="handleChange"
>
  <other-component></other-component>
  <other-component></other-component>
</component-name><component-name
  :disabled="disabled"
  class="some-classes"
  :value="value"
  @change="handleChange"
/>
<other-component></other-component>
<other-component></other-component>
复制代码

如果是下面这样的形式,至少在我看来,想要一下子找到 component-name 这个标签的属性部分,是不那么直观的。

在脑子里,我需要对比一下这块区域,然后明白,“哦,这块代码是一起的”。

// first
<component-name
  :disabled="disabled"
  class="some-classes"
  :value="value"
  @change="handleChange"/>
<other-component></other-component>
<other-component></other-component>
​
// second
<component-name
  :disabled="disabled"
  class="some-classes"
  :value="value"
  @change="handleChange">
  <other-component></other-component>
  <other-component></other-component>
</component-name>
复制代码

针对函数调用、JSX 也是同理,我会让 () 两个括号对齐,来形成一个区域,来“封闭”一块内容。

this.selectedRooms.forEach((block) => {
  block.children.forEach((room) => {
    this.resetRowPrice(room);
  });
});
​
function isMoreThanCurrentDate() {
  return (
    time.getTime() > new Date().getTime()
  );
}
复制代码

让每行代码都有它的含义

除了上面两种情况,你甚至应该让每一行代码都有它自己的意思。

比如下面的代码。

对于组件属性,每一行代表着设置某一个属性,只需要按照纵向扫过去,有没有设置哪些属性将会一目了然。

而方法的调用,第几个参数传的是什么内容,也能直观的看到。

<component-name
  :disabled="disabled"    // 设置禁用属性
  class="                 // 设置class
    class1
    class2
  "
  :value="value"          // 绑定 value
  @change="handleChange"  // 绑定 change 事件
/>

renderSomeComponent(
  id,
  getName(someCondition, anotherCondition),
  age,
  icon,
  getAddress(id, name, age)
)
复制代码

下面这样的代码都是我项目中实际遇到过的,你不能一下子看到组件有哪些属性,方法第几个参数是什么。

你必须得一个个看过去,甚至还需要区分哪一部分是调用方法获得的。

<component-name :disabled="disabled" class="class1 class2" :value="value" @change="handleChange">
  
renderSomeComponent(id, getName(someCondition, anotherCondition), age,
  icon, getAddress(id, name, age))
renderSomeComponent(anotherId, getName(someCondition, anotherCondition), age,
  icon, getAddress(anotherId, name, age))
复制代码

4 适当注释

当代码能够清楚表达它所做的事情的时候,是不需要额外的注释来为它做解释的,但不代表着永远不需要写注释了。

代码只能向阅读者传达出这段代码的作用,但是为什么要这么做,却只能通过注释来向读者说明。

这是一个项目中表格列宽自适应部分的代码,这里的注释说明了为什么要这么做的内容——防止单元格内容被遮挡。

// 计算表格列宽。
// 防止单元格内容因宽度不够,被遮挡的问题。
calculateCellWidth(currView) {
  // 计算单元格列宽代码
  this.checkTableWidthGap();
},
  
 // 在 body 宽度与表头宽度不足一个单元格时,将剩余宽度补足。
checkTableWidthGap() {
  // 列宽补足代码
},
复制代码

还有一些程序员之间用来交流的注释?。

/***
 * 这个公司没有年终奖的,兄弟别指望了,也别来了,我准备辞职了
 * 另外这个项目有很多*Bug* 你坚持不了多久的,拜拜!
 */
复制代码

5 单一职责

一个文件维护一类代码、一个方法维护一个功能、一块区域里的代码做一件事、一行代码只代表一个意思。

当遵循这么一个简单的理念,从一行代码到整个项目,都会有着自己的含义。

前面说的将代码模块化也是为了保持每个模块的职责单一。

6 减少重复

当代码中出现重复的部分的时候,就是考虑拆分的时候了。

相关属性及方法拆分到一起,就是
模板+逻辑+样式拆分到一起,就是组件
功能相关的代码拆分到一个文件,就是模块文件

类、组件、模块、框架,或者说封装操作,正是为了聚合关联性强的代码,使之形成一个又一个的“模块”。

而这些封装操作将大块代码聚合在一起,正是为了保持职责单一,并且后续如果需要进行维护或功能扩展时也会变得方便。

比如遍历数组元素并设值。对于下面这样的代码,完全可以在循环体顶部声明一个变量然后取值。

这里只有两行,我遇到过七八行代码都是这样的,当取值逻辑变化的时候,比如变成 i + 1,那所有取值的部分都需要修改一次。

for (let i = 0; i < people.length; i++) {
  somebody.id = people[i].id;
  somebody.name = people[i].name;
}
复制代码

拆分方法也是同理,如果某一段相同的代码逻辑需要变动,那么所有地方都得修改一次。

如果我们要对内部进行改造,只要保持整个模块内部的功能是一致的,也不用担心原有功能受到影响。

代码质量指标

在最后,我想给你列出这 7 个衡量代码质量的指标。

比较公认的标准有7个:

  • 可维护性(maintainability)
  • 可读性(readability)
  • 可扩展性(extensibility)
  • 灵活性(flexibility)
  • 简洁性(simplicity)
  • 可复用性(reusability)
  • 可测试性(testability)

其中,可维护性、可读性、可扩展性又是提到最多的、最重要的三个评价标准。

结语
今天分享的这六个理念足够让你打下一个写好代码的基础,至于代码要不要写分号、括号前后要不要空格、一行代码不能超过多少列等这样的要求交给 lint 工具吧。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享