你可能忽视的细节-代码整洁之道(下)

最近被领导安排,参加了几次codeReview,发现了不少不大不小的问题。因为是另一个小组的,所以我基本上没怎么关注业务逻辑,主要重点放在了对代码风格、抽象层次等的关注,随之发现了一堆问题。

正好我最近在看《代码整洁之道》这本书,因此我有了整理这方面的一些想法。

本系列将分为上中下三篇,分别从纯理论方向实践层面来对代码整洁之道进行个人的一些见解说明。

代码

在第一篇大概介绍了一下关于代码整洁程度的重要性、如何保持代码的整洁等几个宏观的方面。第二篇,我们将视角放眼于各个应该注意的方面,并对其进行了描述。

在本篇,我们将会针对第二篇所说的那些重点,来进行代码层面的概述,给大家更为直观的体现。(所选语言为javascript

回顾一下上一篇所关注的重点

  • 命名
  • 函数
  • 注释
  • 代码格式
  • 错误处理
  • 边界
  • 单元测试
  • 迭进设计
  • 逐步改进

命名

  • 提防使用外形相似度较高的名称
// bad
const isLoading1 = false
const isLoading2 = false

// good
const isLoadingForToast = false
const isloadingForTable = false
复制代码
  • 使用可以读的出来的名称,而不是某种简写
// bad
function test(a, b, c) {
  doSomeThing
}

// good
function test(name, age, sex) {
  doSomeThing
}
复制代码

对于函数的形参,两种命名所造成的后期维护压力差别是巨大的。

试想一下,你要接手别人的代码,别人的代码中,参数命名尽是abc这种毫无表述意义的标识符,得有多绝望。如果命名可以很好的表达清楚所代表的意思,那么不管是对于自己的开发,还是别人的维护,都有足够大的好处。

  • 使用可以搜索的名称,比如定义常量
// bad
if (obj.contentType === 'application/json') {
  xxx
}
a.contentType = 'application/json'

// good
if (obj.contentType === ContentType.APPLICATION_JSON) {
  xxx
}
a.contentType = ContentType.APPLICATION_JSON
复制代码

对于这种不会进行改变的值,直接使用常量进行声明,维护统一的一个值,而不是在需要修改的时候满世界的找哪儿使用了。

函数

  • 函数应该尽量短小

  • 函数应该遵循单一职责原则,也就是函数应该只做一件事,做好这件事,并且只做这一件事

// bad
async function init() {
  const name = await getName()
  // doSomethingForName...
  const age = await getAge()
  // doSomethingForAge...
  const sex = await getSex()
  // doSomethingForSex...
}

// good
async function changeName() {
  const name = await getName()
  // doSomethingForName...
}

async function changeAge() {
  const age = await getAge()
  // doSomethingForAge...
}

async function changeSex() {
  const sex = await getSex()
  // doSomethingForSex...
}

function init() {
  changeName()
  changeAge()
  changeSex()
}
复制代码

函数只做一件事是为了更好的解耦程序。

bad示例中,init函数里面混杂着nameagesex三种逻辑,全部混杂在一起,不仅函数体积过大,对于维护来说,不仅要考虑三种逻辑的边界,还得考虑他们是否存在某种关联。

good示例中,将nameagesex三种逻辑进行拆分,分别拆分成为三个独立的函数,各自负责自己相关的逻辑,init函数只负责调用它们。不仅方便自己进行逻辑的解耦,更方便后来的维护者对逻辑的梳理。

  • 不要害怕长名称,长而具有描述性的名称,要比短而令人费解的短名称好,要比描述性的长注释好
// bad
// 这是一个用于用于判断是否是女性的函数
function itpaw(person) {
  return person.sex === 'woman'
}

// good
function isThisPersonAreWoman(person) {
  return person.sex === 'woman'
}
复制代码

bad示例的名称是一个简称,作者可能为了防止别人看不懂,或者防止自己也忘了这个的意思,增加了一条用于描述的注释,可是即使有注释,别人在其他地方进行引用的时候,也需要思索半天那个函数叫啥。反之good示例直接使用了代表他功能的函数名称,既不需要增加额外的注释,又能让人有印象,可以很快的想起。

  • 函数的复杂度往往与参数的个数成正比,因此函数参数应该尽可能的减少,不要多于三个,多余三个应当改为使用对象传参或者考虑是否应该拆分
// bad
function add(name, age, sex, height, hobby) {
  doSomeThing...
}

// good
function add(person) {
  const { name, age, sex, height, hobby } = person
  doSomeThing...
}
复制代码

函数的参数是函数对外暴露的一个重要的点,如果参数的数量过多,可能会提高使用者的心智负担,因此尽量减少函数的参数,对于函数的封装,往往是很有必要的。

  • 尽量使用纯函数,纯函数具有更好的可维护性,更健壮
// bad
let age = 12
function fn() {
  doSomeThing(age)
}

// good
function fn(age) {
  doSomeThing(age)
}
复制代码

bad示例中,fn函数内部引用了暴露在外的age属性,这是不安全的,因为你不知道是否还有其他更改age的可能,对于这种情况来说,doSomeThing是无法掌控的。

而在good示例,doSomeThing所引用的age,是通过fn函数传递进来的,也就是我们不需要考虑其他更改age的可能,我们的目标只关注于age

注释

注释更多的在于个人的把握,我见过好多类库声明几个属性做了上百行的注释,也见过这些类库很复杂的逻辑操作一点注释不进行添加的,对于注释,更多需要的可能还是对于重点的关注。

代码格式

  • 代码块之间留行分割
// bad
function fn() {
  const b = 1
  if (xxx) {
    xxxxx
  }
  c.name = 123
}

// good
function fn() {
  const b = 1
  
  if (xxx) {
    xxxxx
  }
  
  c.name = 123
}
复制代码

对于代码的可读性来说,代码块或者逻辑直接进行留白,是极为重要的。

  • 关系密切的概念应该互相靠近
  • 变量声明尽可能靠近其使用位置
// bad
export default {
	data() {
		return {
			a: 1,
      b: 2
		}
	},
  methods: {
    changeA() {
      this.a = 2
    },
    changeB() {
      this.b = 1
    }
  },
  mounted() {
    this.changeA()
  }
}

// good
function someThingWithA() {
  const a = ref(1)
  const changeA = () => a.value = 2
  onMounted(changeA)
  
  return {
    a,
    changeA
  }
}

function someThingWithB() {
  const b = ref(2)
  const changeB = () => b.value = 1
  
  return {
    b, 
    changeB
  }
}

export default {
  setup() {
    const { a, changeA } = someThingWithA()
    const { b, changeB } = someThingWithB()
    
    return {
      a,
      changeA,
      b,
      changeB
    }
  }
}
复制代码

这个代码块,我使用了vue来举例子

bad示例中的代码片段是vue2.0的,options风格决定了他的所有逻辑只能使用固定的写法,写在datamethodsmounted等规划好的模块中,但是这也决定了他无法解耦。a相关的逻辑分在datamethodsmounted三个模块中,b相关的逻辑分在datamethods两个模块中,而ab的模块还必须紧密的写在一起,造成维护以及理解上的困难。

good示例中的代码,是vue3.0的,composable的风格,使用函数,将每个功能进行拆分,最后统一进行处理,someThingWithA只负责a相关逻辑,someThingWithB只负责b相关逻辑,达到更好的解耦,便于理解、维护。

备注:可能有点跑偏,这并不算是代码格式的问题,只是为了演示一下可能会出现的情况。

关于这块,可以去看看大帅老猿的这篇文章juejin.cn/post/689054…,里面很详细的介绍了composition-api相对于options-api的变化

  • 若某个函数调用了另一个,就应该把他们放到一起,而且调用者应尽可能放在被调用者上面
// bad
function c() {
  d()
}

function a() {
  b()
}

function d() {
  console.log(123)
}

function b() {
  c()
}

// good
function a() {
  b()
}

function b() {
  c()
}

function c() {
  d()
}

function d() {
  console.log(123)
}
复制代码
  • 使用短行代码(最多80列或120列)

这个实际上就是一个功能的拆分,将一个大函数,拆分成颗粒度细微的小函数,便于维护。

  • 运算符之间应该存在空格
// bad
const a=1
a+=1
a**=2

// good
const a = 1
a += 1
a **= 2
复制代码
  • 应该使用统一的缩进
// bad
function a()
{
  if(xxx) {
xxx
  }
}

// good
function a() {
  if (xxx) {
    xxx
  }
}
复制代码
  • 团队应该使用统一的代码风格

这点是最最最重要的!!!

这点是最最最重要的!!!

这点是最最最重要的!!!

错误处理

  • 使用统一的错误处理模块
  • 对错误进行分类处理
  • 对各种错误信息应该有着详尽的描述
// bad
async function getName() {
  try {
    xxxx
  } catch (e) {
    if (e.code === 400) {
      xxx
    } else if (e.code === 500) {
      xxx
    }
  }
}

// good
// catchError.js
const networkErrorMap = new Map([
  [404, 'not found'],
  [500, 'server error'],
  xxxx
])

function catchNetworkError(networkError) {
  let errMsg = networkErrorMap.get(networkError.code)
  if (!errMsg) {
    errMsg = '未知错误!'
  }
  
  return errMsg
}

export function catchError(e, cb) {
  let msg = ''
  switch(e.type) {
    case 'networkError':
      msg = catchNetworkError(e)
      break
    case 'xxx'
      xxx
  }
  
  toast(msg)
  if (isFunction(cb)) {
    cb(e)
  }
}

// index.js
import { catchError } from 'catchError.js'

function doSomeThing(err) {
  xxx
}

async function getName() {
  get().catch(e => {
    catchError(e, doSomeThing)
  })
}
复制代码

小结

其实那些要点,光代码层面的,大概也就这么多,后面的边界单元测试迭进设计逐步改进之类的,都更偏向于约定以及习惯了,在这里,就不展开了。

代码的整洁,不管对于个人,还是对于团队,都是相当重要的一环。

而怎么保持,就需要各位自行努力了。

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