Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

如何写好函数? #11

Open
luoway opened this issue Aug 11, 2019 · 0 comments
Open

如何写好函数? #11

luoway opened this issue Aug 11, 2019 · 0 comments
Labels
methodology 方法论

Comments

@luoway
Copy link
Owner

luoway commented Aug 11, 2019

什么是好的函数?

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

  • 可执行

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

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

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

  • 完成功能

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

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

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

  • 可阅读

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

    试想一下反面例子: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(){
  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,结合子函数名的语义理解功能逻辑。

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

结束语

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

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
methodology 方法论
Projects
None yet
Development

No branches or pull requests

1 participant