所得税代码怎么算最小

更新时间: 2024.09.19 03:28 阅读:
》中有过深入介绍。本文重点介绍一些降低代码认知负载的具体方法。

认知负载和工作记忆

在探讨具体的方法之前,首先要简述下,认知负载是如何影响工作效率的。认知负载理论中最重要的概念当属大脑的工作记忆(如下图所示),这是用来处理各种运算和逻辑推理的地方,有点类似计算机的内存,用于临时存储一些概念。相比于长时记忆,工作记忆空间极为有限,也是我们学习和工作的瓶颈所在。

所得税代码怎么算最小(图1)

图1 认知负载和工作记忆 (图片来自网络)

一般而言,工作记忆能够同时处理的概念数量是7±2(也就是5到9这个范围),而且,这是很难改变的。所以,不管是小学生,还是科学家,心算2位数的乘法对他们而言,难度应该是差不多的。另外,我还发现,如果几个概念互相之间具有较强的关联,会更容易被记住。因此,软件开发中,一个非常重要的原则,就是尽可能减少同一时刻大脑需要处理的概念数量,尤其是那些不相关概念的数量。这些概念包括但不限于:变量名、方法名、方法参数、返回值、条件判断、类型信息等等。

另外,当前要解决的问题(比如这个方法的返回值为什么不符合预期)或者要实现的目标(比如实现某个特定的业务功能),也都会占据工作记忆的空间,因此,留给其他概念的空间就更少了。

如果同一时刻要处理的概念太多,大脑就会超载,之前记住的一些概念就会被清除出去(有点类似于缓存的清除策略)。表现出来的现象就是,看代码的时候,经常看到后面忘了前面,需要倒回去再来一遍。甚至有时候需要反复看多遍,直到将这段逻辑载入长期记忆(有点类似交换空间)。可想而知,这种情况下,对效率的影响有多大。

软件开发中,在宏观的架构层面,可以使用关注点分离的思想,让我们某个时刻只需要关注某个较小的模块,同时尽可能减少与其他模块的关联;除此之外,在具体方法和代码层面,也有一些值得注意的小技巧,能够显著提高代码的可理解性和可维护性。

面向认知负载的代码调优

起个好名字

好的名字,能够清晰地表达概念的含义和作用,让我们在读代码的时候不需要花费额外的认知能力去思考,从而降低了认知负载。好的命名需要具备相关性、精确性和区分性。

相关性,指的是名字需要与其所表示的概念强相关,并且是在当前的上下文中相关。对于表示业务概念的变量名和方法名,不要自己随意命名,而是要尽量用专门的领域相关词汇(有些研发同学对于自己不了解的领域概念,可能直接用机器翻译出来的单词,很容易失去相关性),对于技术相关的名字,最好能够体现出背后的设计决策和模式(xxxFactory、xxxPipeline等),那些不具备相关性的名字,很容易被逐出工作记忆,从而影响对代码的理解。

精确性,指的是名字所指的概念和范畴最小化,没有隐藏含义,不需要二次思考。我曾经在某家公司负责交易系统,我们上游的促销系统中,有个用来表示优惠活动适用对象(比如,是针对商品还是运费)的字段,叫bizType,让人云里雾里,不知道其前缀biz所指为何物。在业务系统中,凡事皆可叫biz,加不加这个前缀,几乎没有区别,因此,这个名字的含义太宽泛了,应该缩小范畴(比如改成targetType,就是一个更好的选择)。太宽泛的命名,需要额外的思考,而额外的思考也会占据宝贵的工作记忆,大大降低效率。

区分性,指的是多个名字之间不能引起混淆,尤其当它们同时出现的时候。还拿上面促销系统举例,就在bizType所在的同一个类中,还有一个字段叫promotionType,表示优惠的计算方式(比如,是打折还是直减等),这两个字段名的区分性就不好。还有个更加极端的例子,在我们交易系统中,用来表示商品优惠金额和优惠后的价格这两个概念的字段,分别叫skuPromotionAmout和skuAmountPromotion,是不是这么对应的可能我记错了,因为当时我就记不住。这样的命名,不仅需要额外的思考,还会把名字与之接近的概念一股脑儿全引入工作记忆,让大脑不堪重负。

延迟加载概念

变量声明或定义,应该尽可能靠近它被使用到的地方,这样既可以推迟概念被加载到工作记忆中的时机,也能够减少同一时刻驻留在工作记忆中的概念数量,相当于给大脑减压。另外变量定义和使用放在一起,也能够让这两处代码形成一个完整的概念,从而进一步降低认知负载。其实对于计算机内存来说,也是类似,推迟定义变量的时机,也是一种GC友好的编码方式。最糟糕的定义变量的方式,莫过于在函数或方法最开始的地方,一股脑儿把需要用到的变量全部定义或声明出来,这种方式,一上来就把我们的工作记忆塞满,等到读到使用变量的地方,很有可能已经忘记了变量含义,又得回过头去温习,效率非常低下。看一个假想的例子,以下两种写法,运行结果完全相同,但是认知负载差异巨大:

所得税代码怎么算最小(图2)

图 2 通过安排变量位置降低认知负载

上图左边的写法,大脑里面同一时刻需要处理的概念数量最大可以达到10个(记住入参和这个方法的目的包含在内)并且定义的地方和使用的地方相隔较远,这意味着我们需要保持这种高强度的认知压力较长时间;而右边的写法,同一时刻需要处理的概念数量不超过5个,且需要时才加载进工作记忆,读起来就非常轻松。

如果业务逻辑确实比较复杂,必须要定义很多变量,还是有办法可以降低认知负载的,那就是把逻辑上相似或者互相关联的变量归为一组,放在一起,用空行隔开,以突出分组的用意。这样安排,是利用了邻近性法则,使得大脑自动把相似的物体归为一类,减少需要记住的概念数量,降低认知负载。

提前回收概念

很多方法在执行处理逻辑之前,都要先进行很多校验,比如判断入参是否合法,判断权限是否足够,判断当前资源的状态等等。一种写法是使用多层if嵌套——所有条件都满足后,在最内层的if语句块中写处理相关的代码,这就会形成“箭头型”代码。

所得税代码怎么算最小(图3)

图3 箭头型代码 (图片来源于网络)

这种写法的问题在于,每一层if判断都会形成认知负担,并且这个负担需要一直带下去 ——你得时刻记住,在哪些条件下才执行到当前位置。如果一些if条件本身很复杂,那情况会更糟,你甚至还没读到真正执行处理逻辑的位置,就精疲力尽了。

更好的方式,是使用“卫语句”,把嵌套的if语句写成多个平级的形式,每当一个if条件不满足的时候,就直接返回(或报异常)。这样做的好处,是每看完一个if语句块,你就可以安全地在思想上放下它(从工作记忆中移除),相当于做了一次“垃圾回收”,然后,就可以继续轻装上阵,更好地理解后面的逻辑。这样写出来的代码,虽然方法可能会长一些,但是理解起来更轻松。

所得税代码怎么算最小(图4)

图4 箭头型代码 vs 卫语句

上图中这个示例,我们假设if条件很简单,都只有一个变量,那么,左边的方式,当你看到内层执行处理的代码时,工作记忆里面此时需要记住的概念数量是6(算上方法名和参数名),而右边的方式,工作记忆中同一时刻的概念数量不超过3。实际中,差异可能会更大。

可能很多人都知道“箭头型”代码的问题及其优化方法,不过,只有深刻理解了背后认知方面的原理,才能够灵活运用这个思想,提前给读代码的人减负。比如,最小化变量作用域就是该思想的另一个很好的实践,我们应该尽可能把变量定义在使用它的最内层的块中(比如if、for、try块等)中, 另外,在很多语言中,甚至可以单独使用{}来显式定义一个块,块中的变量只在块中有效,块结束后,其中的变量也就不可见了。

所得税代码怎么算最小(图5)

图5 块作用域

上图示例代码中使用了显式的块作用域,块结束后,我们大可以放心地“忘掉”其内部出现过的变量a和b,因为我们知道后面再也不会用到了,这样就可以提前给大脑减负。(熟悉垃圾回收机制的同学可能也知道,这种写法还能够提示垃圾回收器,块内部的变量可以被提前回收掉了。)

最小化方法传参

上文提到过,读代码时,方法调用的参数也是需要载入工作记忆中的概念,因此,写代码时,如何传参,也很有讲究。

减少显式概念传递

方法参数是概念,方法传参本质上就是概念的传递,而概念是需要消耗认知资源的, 所以,应该尽可能减少方法参数。那些有着10个以上参数的方法,光是理解方法签名,就够我们喝一壶的了。因为难以理解,也就难以使用,还有可能被误用,尤其是在多个参数类型相同的情况下。

首先要避免的就是传递冗余参数。如果某个参数能够根据其他参数算出来(或者进行额外查询就能获得),就是冗余的,应该考虑去掉。(有时候,出于性能方面的考虑,可以做些妥协)。

在确实需要传递很多参数的业务场景下(通常是创建一些业务资源,比如用户、商品、订单等),应该把多个参数封装成对象(比如CreateOrderRequest)进行传递。很多时候,我们其实是在浏览代码,不需要搞清楚遇到的每个方法的细节,而仅仅是为了找到我们真正感兴趣的部分,所以这种封装参数的做法,能够显著地降低认知负载。

如果是对象的构造方法需要大量参数,可以考虑使用Builder模式,通过链式赋值,一次赋值一个字段,既增加了可读性,也能够避免出错。

减少隐式概念传递

对象参数,如果被不合理地使用,反而会增加认知负载。对象参数中的字段,是隐藏在对象后面的概念,如果我们点开这个对象,想看看方法调用的细节,这些概念也会被加载到工作记忆。有时候,某个方法实际只用到了入参对象中很少的几个字段,却要求传入整个对象,通常,这都是不合理的。

举个例子,假设在创建订单的场景中,需要调用一个计算运费的方法,该方法只需要用到用户地址id、商品总件数和商品总重量这三个字段,那么传递包含大量其他字段的CreateOrderRequest对象就不太合适,因为光看参数不看方法实现的话,你不知道计算运费需要用到哪些信息,很多原本不需要的字段,成了干扰和负担。另外,这种简单粗暴的传参方式,也会导致方法难以使用和测试——在拿不到CreateOrderRequest对象的场景下,直接实例化一个新对象,仅仅赋值需要的那几个字段后传下去,这既不方便,也埋下了坑。

避免不必要的函数调用和过深的嵌套

函数的最佳代码行数是多少,或者不应该超过多少行,这两个问题一直没有确切的答案。真相是,这本身是一个伪命题,真正的问题是,函数如何实现,认知负载最小。上文我们说过,有时候,长一些的函数,反而比短一些的函数更容易理解,只要它同时塞进我们工作记忆中的概念更少。

函数调用也是有开销的。对于计算机来说,得保留调用点的上下文,同时为被调函数创建新的上下文,函数返回后得恢复之前保留的上下文;对于人脑来说,也是类似的过程,进入新的函数后,在我们的工作记忆中也会发生上下文切换。如果被调用函数做了很多的事情,封装了足够的复杂性,调用这个函数就是值得的,因为总体而言,认知负载被降低了(所有概念不至于全部在一个函数里面,一股脑儿塞进工作记忆);相反,如果,被调函数仅仅只做了很简单的事情,或者相对实现代码来说,参数非常多,就有点得不偿失了。

如果实现逻辑不是很复杂,不断地调用嵌套的函数,一次只做一点事情,反而给读代码的人增加障碍,就像话不一次性说完,不断卖关子一样。

另外,有些时候,我们看代码,就是想搞清楚某个细节的来龙去脉,或者查清某个bug的根本原因,因此,需要不断跟到函数调用的深处。就像过深的调用堆栈会让程序报StackOverflow异常一样,过深的函数调用,也会让大脑认知超载。

总结

限于篇幅,本文并没有面面俱到地覆盖所有我能想到的点,另外,写得太多,可能也会让大家认知超载,反而得不偿失。重要的是,希望越来越多的程序员们,能有这个意识,不仅要面向计算机优化自己的代码,更重要的,是面向我们的大脑优化代码,让代码更容易理解一些,造福自己,也造福他人。

更多精彩文章,欢迎关注微信公众号:技术凌云

•••展开全文
标签: 暂无
没解决问题?查阅“相关文档”