如何写好函数?

时间:2021-1-8 作者:admin

什么是好的函数?

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

  • 可执行

    这是最基本的,函数不能运行那就没有意义。

    保障函数可执行,要从两个方面考虑:函数本身逻辑、函数执行环境。函数本身逻辑可执行不用多说,函数执行环境是容易遗漏并出错的:函数如果接收参数,那么就要考虑参数的数据类型是否符合运行要求;函数如果调用外部变量、函数,就要考虑外部变量是否存在且符合要求,外部函数是否能正常工作。对这些情况的处理能力称为健壮性

    换个角度考虑,如果写的这个函数在程序中没有被调用过,那它就是应当删除的冗余代码,应当减少。如果这个函数被调用一次以上,它就是有价值的代码。如果被多次调用,那它就具备复用性,价值进一步提升。

  • 完成功能

    这是第二个基本,函数没有完成它该有的功能,那它的意义也是值得怀疑的。

    进一步考虑,如果函数没有完成被期望的功能,却干了别的出人意料的事,那它简直是老鼠屎,扰乱了程序的执行逻辑。

    提炼一下:“被期望的功能”意味着函数是有姓名的,在函数名中应当体现出来,这就是语义。函数不应当做出“别的出人意料的事”,这就是副作用,应当避免。

  • 可阅读

    衡量可阅读程度的名词,一般称为可读性。可读性是现代程序语言发展的根本,从二进制,到汇编等低级语言,到今天百家争鸣的高级语言,可读性一路攀升。按理说,高级语言的可读性已经远高于低级语言了,为什么编程时还要注意可读性?

    试想一下反面例子:Web前端如何保护代码资产?

    就目前客户端浏览器“三大件”HTML、CSS、JavaScript而言,保护代码资产是不可能实现的。所有的解决方案归纳为“降低可读性”,让人难以阅读,就一定程度上做到了保护代码资产,不让人理解进而进行修改和维护。

    相反地,提高可读性,就是为了方便自己或他人理解以及进行修改和维护。

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

  • 可执行的,健壮的,冗余代码越少越好,复用性越高越好。
  • 完成功能,函数名是有语义的,说明了函数完成的功能,且没有副作用。
  • 可阅读的,方便再次理解、修改和维护。

怎样写好函数

本文以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(callback){
  return transformPromise(doSomeThing)
}
function work(callback){
  return transformPromise(doSomeThing)
}
function goHome(callback){
  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,结合子函数名的语义理解功能逻辑。

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

结束语

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

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

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

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。