什么是好的函数?
这要从结果上来评价一个函数的好坏。先考虑写完一个函数,它有哪些结果?
-
可执行
这是最基本的,函数不能运行那就没有意义。
保障函数可执行,要从两个方面考虑:函数本身逻辑、函数执行环境。函数本身逻辑可执行不用多说,函数执行环境是容易遗漏并出错的:函数如果接收参数,那么就要考虑参数的数据类型是否符合运行要求;函数如果调用外部变量、函数,就要考虑外部变量是否存在且符合要求,外部函数是否能正常工作。对这些情况的处理能力称为健壮性。
换个角度考虑,如果写的这个函数在程序中没有被调用过,那它就是应当删除的冗余代码,应当减少。如果这个函数被调用一次以上,它就是有价值的代码。如果被多次调用,那它就具备复用性,价值进一步提升。
-
完成功能
这是第二个基本,函数没有完成它该有的功能,那它的意义也是值得怀疑的。
进一步考虑,如果函数没有完成被期望的功能,却干了别的出人意料的事,那它简直是老鼠屎,扰乱了程序的执行逻辑。
提炼一下:“被期望的功能”意味着函数是有姓名的,在函数名中应当体现出来,这就是语义。函数不应当做出“别的出人意料的事”,这就是副作用,应当避免。
-
可阅读
衡量可阅读程度的名词,一般称为可读性。可读性是现代程序语言发展的根本,从二进制,到汇编等低级语言,到今天百家争鸣的高级语言,可读性一路攀升。按理说,高级语言的可读性已经远高于低级语言了,为什么编程时还要注意可读性?
试想一下反面例子: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,结合子函数名的语义理解功能逻辑。
上面的问题是一种影响可读性的典型问题。可读性需要注意的问题不止一种,还有些问题可能存在争议需要统一意见,因此有着“代码风格”之说,不同风格有差异也有共同之处,多做了解和比较,整理出自己心目中的最佳实践吧!
结束语
“如何写好函数”是一个偏主观的话题,在编程实践中程序员们积累了大量客观的评价指标,其中有些指标可能是相互制约的,例如复用性、可扩展性、可读性,三者就不容易共同提高。所以这类问题鲜少有“最佳实践”的讨论。
但是,写好函数的重要性是不言而喻的。“编程一时爽,重构火葬场”,坏的函数要么影响程序员上班的心情,要么提前下次重构的计划到来,两者都不是什么好事。何以解忧?唯有换行。嗯,换行是有条提升可读性的代码风格规范。
反观自身,如何评价自己的代码好不好?笔者的建议是,阅读当前编程语言最流行的一些框架、库的源码,阅读过程中去思考如果自己来写,能不能写得更好。本文正是读源码过程中有感而发。