luoway / blog

个人博客,issues管理
19 stars 3 forks source link

如何写好函数? #11

Open luoway opened 5 years ago

luoway commented 5 years ago

什么是好的函数?

这要从结果上来评价一个函数的好坏。先考虑写完一个函数,它有哪些结果?

由此,一个好的函数,它应当是

怎样写好函数

本文以JavaScript为例,从健壮性、复用性、语义、副作用、可读性五个方面举例说明。

健壮性

坏的例子

function numberPlusOne(val){
  return val + 1
}

期望是对输入数字,返回数字加1后的结果。但如果输入的不是数字,而是数字字符串,或者是非数字的其他内容呢?

好的例子

function numberPlusOne(val){
  if(typeof val === 'string') {
    val = parseFloat(val)
  }
  if(typeof val === 'number'){
    if(!isNaN(val)) return val + 1
  }
  return NaN
}

如果有大数相加需要,还得进一步考虑JavaScript计算精度问题。

复用性

坏的例子

function formatProductPrice(productInfo){
  if(!productInfo) return productInfo
  if(productInfo.price){
    if(typeof productInfo.price === 'string') {
      productInfo.price = parseFloat(productInfo.price)
    }
    productInfo.price = isNaN(productInfo.price) ? '0.00' : productInfo.price.toFixed(2)
  }
  //复制粘贴得到下一段,并替换price为originalPrice
  if(productInfo.originalPrice){
    if(typeof productInfo.originalPrice === 'string') {
      productInfo.originalPrice = parseFloat(productInfo.originalPrice)
    }
    productInfo.originalPrice = isNaN(productInfo.originalPrice) ? '0.00' : productInfo.originalPrice.toFixed(2)
  }
  return productInfo
}

期望是格式化产品的两个价格字段price、originalPrice,两个字段处理方式一致。

好的例子

function formatProductPrice(productInfo){
  if(!productInfo) return productInfo
  formatPrice(productInfo, 'price')
  formatPrice(productInfo, 'originalPrice')
  return productInfo
}

function formatPrice(obj, key){
  if(!obj[key]) return

  let val = obj[key]
  if(typeof val === 'string') val = parseFloat(val)
  obj[key] = val.toFixed && val.toFixed(2) || '0.00'
}

复用性的基本内容就是避免重复代码。但在编程过程中,它应当是值得考虑的优化方案,而不是奉为圭臬的必须方案。提前考虑复用,结果由于各种原因没有被复用到,实际是没有提高复用性,反而可能降低开发效率。

语义

坏的例子

function add(a, b){
  return a + b
}

期望是计算两数相加(add)的结果,即求和(sum)。

好的例子

function sum(a, b){
  return a + b
}

那么add应当如何满足其语义呢?

Number.prototype.add = function(val){
  return this + val
}

let a = 1, b = 2
a.add(b)    //3

add语义是“增加”,sum语义是“合计”,意义是不同的。编程所需的语义,是建立在能够正确理解语言意义基础上的。所以说,程序员是需要学好英语的。 上例说明的是函数名的语义不恰当问题,编程中常见的问题是给常量、变量、字段命名,有时候还会纠结多个相似的值,如何区分命名。

副作用

//对象合并
const obj1 = { a: 1 }
const obj2 = { b: 2 }

function extendWithSideEffect(obj1, obj2){
  Object.assign(obj1, obj2)
  return obj1
}

function extend(obj1, obj2){
  return Object.assign({}, obj1, obj2)
}

期望是“对象合并”,两个函数都实现了对象合并,并返回合并后的对象。extendWithSideEffect的副作用是会改变输入参数obj1对象内容,在当前期望中是副作用,应当避免。

可读性

坏的例子

function oneDayOfWorker(){
  init()    //非常想吐槽的函数名init
}

function init(){
  leaveHome()
}
//假设以下行为均是异步的
function leaveHome(){
  doSomeThing(work)
}
function work(){
  doSomeThing(goHome)
}
function goHome(){
  doSomeThing(sleep)
}

好的例子

function oneDayOfProgramer(){
  leaveHome(()=>{
    work(()=>{
      goHome(sleep)
    })
  })
}

function leaveHome(callback){
  doSomeThing(callback)
}
function work(callback){
  doSomeThing(callback)
}
function goHome(callback){
  doSomeThing(callback)
}

更好的例子

async function oneDayOfProgramer(){
  await leaveHome()
  await work()
  await goHome()
  sleep()
}

function transformPromise(fn){
  return new Promise(resolve=>{
    fn(resolve)
  })
}
function leaveHome(){
  return transformPromise(doSomeThing)
}
function work(){
  return transformPromise(doSomeThing)
}
function goHome(){
  return transformPromise(doSomeThing)
}

这个例子主要说明的可读性问题是,避免“链式”编写函数,而应当以“总-分”的结构去组织函数。

设主函数为main,A、B、C、D是需要有序调用的子函数定义,a、b、c、d是子函数调用。

“链式”编写函数:

main[a], A[b]→B[c]→C[d]→D

描述为主函数中只调用开始的子函数,在子函数定义中去调用其他子函数,形成“链表”结构。代码读者需要逐个子函数地查看以理解主函数main的功能逻辑。

“总-分”结构组织的函数:

main[a→b→c→d], A, B, C, D

描述为主函数中描述了子函数调用顺序,子函数定义各自实现功能。代码读者可以根据主函数main,结合子函数名的语义理解功能逻辑。

上面的问题是一种影响可读性的典型问题。可读性需要注意的问题不止一种,还有些问题可能存在争议需要统一意见,因此有着“代码风格”之说,不同风格有差异也有共同之处,多做了解和比较,整理出自己心目中的最佳实践吧!

结束语

“如何写好函数”是一个偏主观的话题,在编程实践中程序员们积累了大量客观的评价指标,其中有些指标可能是相互制约的,例如复用性、可扩展性、可读性,三者就不容易共同提高。所以这类问题鲜少有“最佳实践”的讨论。

但是,写好函数的重要性是不言而喻的。“编程一时爽,重构火葬场”,坏的函数要么影响程序员上班的心情,要么提前下次重构的计划到来,两者都不是什么好事。何以解忧?唯有换行。嗯,换行是有条提升可读性的代码风格规范。

反观自身,如何评价自己的代码好不好?笔者的建议是,阅读当前编程语言最流行的一些框架、库的源码,阅读过程中去思考如果自己来写,能不能写得更好。本文正是读源码过程中有感而发。