【学习笔记】代码美学:不嵌套主义
记录来源于:【熟】代码美学:为何要成为“不嵌套主义者”-哔哩哔哩 及评论区
本人作为前端开发,仅站在前端角度编写该笔记,感兴趣的朋友可以前往视频总结自己的感受。
一、简介:为什么要有不嵌套主义
不嵌套主义旨在减少函数的嵌套层级结构与过长的代码行数,提高代码的可读性与整体美感。作者提出一个观点,将每次嵌套视为深度 +1 。如下代码示,视频作者表示最多忍受到深度为 3 的函数:
getComputedArr(type)
{ // 深度1
for (let index = 0; index < array.length; index++)
{ // 深度2
if (type == index)
{ // 深度3
const element = array[index];
return element;
}
}
};
当嵌套达到 4 时是什么样子的呢?看以下代码:
getComputedArr(type)
{ // 深度1
for (let index = 0; index < array.length; index++)
{ // 深度2
if (type == index)
{ // 深度3
const element = array[index];
if(index>10)
{ // 深度4
return element;
}
}
}
};
很多朋友觉得似乎还可以接受,但我们的业务代码远远没有那么简单,实际上呈现在你们面前的可能是这种代码:
postAffirmCollection函数
postAffirmCollection(id, isRentant) {
let that = this;
uni.showModal({
content: isRentant ? '是否确认支付?' : '是否确认收款?',
cancelText: "取消", // 取消按钮的文字
confirmText: isRentant ? '确认支付' : '确认收款',
confirmColor: '#175EFF',
cancelColor: '#8F94A3',
success: function(res) {
if (res.confirm) {
if (isRentant) {
that.$clientInterface.rentConfirmPayment(id)
.then((res) => {
this.getSelectBillDetail(this.id);
})
.catch((error) => {
uni.showToast({
icon: 'none',
title: error
})
});
// }
} else {
that.$clientInterface.postAffirmCollection(id)
.then((res) => {
console.log('obj:', res)
that.obj = res.data;
uni.$emit('detail', {
strusDetail: that.strusDetail
});
uni.navigateBack();
})
.catch((error) => {
uni.showToast({
icon: 'none',
title: error
})
console.log(error)
});
}
} else if (res.cancel) {
console.log('用户点击取消');
}
}
});
},//53行 最大深度:7
在这个深度为 7 的长函数中包含了一个确认提示,两个请求数据函数,一个函数包含了三个功能,很难不显得臃肿。对于以上出现的问题,视频作者主要提出了两个解决方案:
- 从主函数中提取子函数
- 判断条件的尽快返回
二、提取子函数
一个过长的函数往往存在着多项功能或是复杂的验证信息,提取子函数就是将这些多项功能拆分成若干个小函数。
1、代码长度判别
对于一个函数是否过长,视频的评论区 @F2·NO 用户给出了如下评判标准:
如何判断一个函数是否需要抽象?可以把“50行”当作一个指标。
20年前一些老派的公司做 CodeReview(代码评审) 时,会把代码打印到纸上。如果哪个函数长到需要翻页才能看全,Reviewer(评审人) 就会摆出这样的表情→😒然后把你挂掉。
我认为这是一个很好的批判标准,例如我在上文提及的业务代码,长度就为 54 行。当你某个函数需要滚动屏幕多次才能完全浏览,这对后期代码维护或是代码重构都会造成很大的问题。
此外我个人还有一个批判标准,在编写代码时我习惯半屏编写代码,当某行代码超出了半屏,就说明嵌套的层级过多,需要提取子函数;或是判断条件过于复杂,需要修改。
2、抽象提取子函数
我们应当如何构建一个合适的子函数呢,视频的评论区 @F2·NO 用户给出了如下评判标准:
如何判断提炼的子函数的好坏?有两个硬指标:
一是子函数依赖的参数个数,二是子函数的复用次数。
子函数复用越多,参数越少,就说明拆解越是有效。
当你提炼出多个子函数,而它们之间使用到的参数有相似之处时,可以将它们进一步抽象成类。
当我们确定一个函数过长后,需要将其中一部分代码提取成为一个子函数,尽可能将每个函数的嵌套深度降低。在日常的业务中,常见的功能块,如提示、验证、跳转等,可以进一步抽象成一个公共函数,提高代码的复用率。
此外需要注意函数与函数间的耦合度,当子函数大量依赖于主函数的值(参数)传递时,应当对参数于功能模块标注清晰明了的注释,方便后期的代码更新迭代与修复。
我们以上文的业务代码为例,我们来简单拆分以下这个函数,我们可以先将确认提示的success回调提取出来:
postAffirmCollection(id, isRentant) {// 判断提示函数
let that = this;
uni.showModal({
content: isRentant ? '是否确认支付?' : '是否确认收款?',
cancelText: "取消", // 取消按钮的文字
confirmText: isRentant ? '确认支付' : '确认收款',
confirmColor: '#175EFF',
cancelColor: '#8F94A3',
success: that.postSuccess(res);
});
},//11行 最大深度:2
让我们来看提取出来的 postSuccess(res) 函数,提取出来的子函数虽然已经简化了提示部分,但是依旧显得杂乱,我们需要进一步将函数进行拆分,提取出满足判断的两个获取数据的函数:
postSuccess(res)函数
postSuccess(res) {
if (res.confirm) {
if (isRentant) {
this.$clientInterface.rentConfirmPayment(id)
.then((res) => {
this.getSelectBillDetail(this.id);
})
.catch((error) => {
uni.showToast({
icon: 'none',
title: error
})
});
}
} else {
this.$clientInterface.postAffirmCollection(id)
.then((res) => {
console.log('obj:', res)
this.obj = res.data;
uni.$emit('detail', {
strusDetail: this.strusDetail
});
uni.navigateBack();
})
.catch((error) => {
uni.showToast({
icon: 'none',
title: error
})
console.log(error)
});
}
} else if (res.cancel) {
console.log('用户点击取消');
}
}// 36行 最大深度:5
提取后,成功拆分为以下三个函数,并且它们的嵌套深度都没有超过 3 。通过对代码的实际拆分,可以验证了视频作者为何将深度 3 作为一个批判标准,嵌套深度小于 3 的函数更具有代码美感,也更好阅读。
postSuccess(res)-拆分函数
postSuccess(res) {
if (res.confirm) {
if (isRentant) {
this.postSuccessTrue();
} else {
this.postSuccessFalse();
}
} else if (res.cancel) {
console.log('用户点击取消');
}
}// 11行 最大深度:3
postSuccessTrue()函数
postSuccessTrue() {
this.$clientInterface.rentConfirmPayment(id)
.then((res) => {
this.getSelectBillDetail(this.id);
})
.catch((error) => {
uni.showToast({
icon: 'none',
title: error
})
});
}// 12行 最大深度:3
postSuccessFalse()函数
postSuccessFalse() {
this.$clientInterface.postAffirmCollection(id)
.then((res) => {
console.log('obj:', res)
this.obj = res.data;
uni.$emit('detail', {
strusDetail: this.strusDetail
});
uni.navigateBack();
})
.catch((error) => {
uni.showToast({
icon: 'none',
title: error
})
console.log(error)
});
}// 18行 最大深度:3
三、尽快返回
当我们对某段代码进行验证时,总是会出现大量的嵌套。面对这种情况,我们往往可以将需要返回的条件置于顶层,一旦满足就会返回,核心代码则无需在做嵌套判断。
1、使用方法
我们先看一个简单的例子,下面的代码就是经典的顺序结构的写法,嵌套深度为 3:
getComputedArr(type){
if(type == 1){
let obj = {item:6};
this.getComputedObj(obj)
uni.showToast({
icon: 'none',
title: '返回超过'
})
}else{
return false;
}
};
让我们来看看不嵌套主义是怎么处理这个函数的呢?当我们将需要返回的条件提前,则可以释放原本需要嵌套的核心代码,视频作者将这种写法称为 验证守护 。此时的嵌套深度降至了 2 ,核心代码也更加清晰明了。
getComputedArr(type){
if(type != 1){
return false;
}
let obj = {item:6};
this.getComputedObj(obj)
uni.showToast({
icon: 'none',
title: '返回超过'
})
};
2、实际应用
在条件简单的代码中,尽快返回显得有点鸡肋,远没有提取子函数更重要,那么接下来我们看看这段代码。我们以上文还未拆分的 postSuccess(res) 为例,看看如何处理。我们通过尽快返回超过减少了一级深度,现在我们可以比较一下上下文的代码,即便现在的深度仍大于 3 代码也清晰明了了许多。
postSuccess(res)函数
postSuccess(res) {
if (res.cancel) {
console.log('用户点击取消');
}
if (isRentant) {
this.$clientInterface.rentConfirmPayment(id)
.then((res) => {
this.getSelectBillDetail(this.id);
})
.catch((error) => {
uni.showToast({
icon: 'none',
title: error
})
});
}
this.$clientInterface.postAffirmCollection(id)
.then((res) => {
console.log('obj:', res)
this.obj = res.data;
uni.$emit('detail', {
strusDetail: this.strusDetail
});
uni.navigateBack();
})
.catch((error) => {
uni.showToast({
icon: 'none',
title: error
})
console.log(error)
});
}// 33行 最大深度:4
接下来我们进一步拆分子函数看看,确实相较于上面的代码更加简洁。尽快返回作用在需要判断的代码部分,可以提高代码的可读性,同时减少代码的深度和行数,是切实有效的方法。
postSuccess(res)-拆分函数
postSuccess(res) {
if (res.cancel) {
console.log('用户点击取消');
}
if (isRentant) {
this.postSuccessTrue();
}
this.postSuccessFalse();
}// 9行 最大深度:2