矫情文学——函数参数数量

746 阅读5分钟

初衷

本系列的主旨在于探讨在工作中遇到的一些代码现象,以及一些自己的见解,旨在分享自己在如何写代码上的一点心得体会。因为见解往往会和现有工程实践不一致(很可能会开喷),故而标为矫情文学,我姑妄说之,诸君姑妄听之,无非一个讨论,不要动了火气。

超级构造函数

先看下我最近看到的一些代码

class TopVC(
private val p1:xxx,
private val p2:xxx,
private val p3:xxx,
private val p4:xxx,
private val p5:xxx,
private val p6:xxx,
private val p7:xxx,
private val p8:xxx?,
private val p9:xxx?,
private val p10:xxx?){
...
}

代码很简单,这里有一个类TopVC,它的构造函数有10几个参数,有些构造参数可空,有些构造参数必传,不知道大家第一眼看到这种写法是什么反应,当看到这个类的构造函数时,我的表情是这样的。

image.png

构造函数,也是函数,一个函数由函数名、入参、出参和函数体构成,在阅读代码的时候,函数名算是我们最直接映入眼帘的,函数名标识了函数的行为,参数往往代表了执行这个行为所需要的元素或者前置条件,当一个函数没有参数时,代码阅读者只需要关注函数行为本身,而如果有很多参数,就要求代码阅读者不仅关心函数行为本身,还要关心前置条件活其他元素,增加理解难度,本质上函数名与参数不处于同一个抽象层级,它要求你了解并不特别重要的细节。 整体看,当写一个超级构造函数或者一个函数参数过多的函数,会带来下面的问题:

  • 可读性降低:过多的构造函数参数会使构造函数的调用和使用变得复杂。调用方需要记住参数的顺序和类型,并确保传递参数的正确性。这会增加代码的复杂性和理解难度,使代码难以阅读和理解。
  • 容易出错:由于参数数量众多,存在传递参数时的顺序错误或者类型错误的可能性。这可能导致代码运行时错误或者逻辑错误,增加调试和排查问题的难度。
  • 维护困难:当需要对类的构造函数进行修改时,需要同时修改所有相关的构造函数调用点,这可能会导致疏漏或者错误。此外,添加或删除构造函数参数时,还需要更新所有相关的代码,增加维护的复杂性。
  • 单例编写困难:加大编写能确保参数的各种组合运行正常的测试用例难度。

当然,有的人会说,我的这个类就是需要这么多的参数,这是业务需要的,怎么破?这个问题确实存在,业务复杂,需要很多能力的注入,但是是否要写这样一个超级构造函数呢?我觉得未必,下面咱们根据情况拆解下。

必需参数和非必需参数

上面的TopVC,有的参数是构造函数非必需的,针对这种情况,其实完全可以通过使用建造者模式、重载构造函数来缩减参数数量。

//重载构造函数
class TopVC(
private val p1:xxx,
private val p2:xxx,
private val p3:xxx,
private val p4:xxx,
private val p5:xxx,
private val p6:xxx,
private val p7:xxx,
private val p8:xxx?=null,
private val p9:xxx?=null,
private val p10:xxx?=null){
...
}{
//建造者
class TopVC private constructor(
private val p1:xxx,
private val p2:xxx,
private val p3:xxx,
private val p4:xxx,
private val p5:xxx,
private val p6:xxx,
private val p7:xxx
){
private var p8:xxx?=null,
private var p9:xxx?=null,
private var p10:xxx?=null

class Builder(
val p1:xxx,
val p2:xxx,
val p3:xxx,
val p4:xxx,
val p5:xxx,
val p6:xxx,
val p7:xxx){

private val topVC=TopVC(p1,p2,p3,p4,p5,p6,p7)

fun setP8(p8:xxx):Builder{
   topVC.p8=p8;
   return this
}

fun setP9(p9:xxx):Builder{
   topVC.9=p9;
   return this
}

fun setP10(p10:xxx):Builder{
   topVC.10=p10;
   return this
}

fun build():TopVC{
   return topVC
}
}
}{

通过上述方法,可以在一定程度上缩短构造函数调用时所传入的参数量,减少因为传参顺序或者类型错误造成的风险问题。

当各个参数之间关联性较强时

之前构造函数里的参数都没有附加概念,这里添加一些具体的名称来举例看下。

class TopVC(
//用户名
private val userName:String,
//用户昵称
private val nickName:String,
//用户头像
private val avatarUrl:String,
...){
...
}

这次TopVC的参数包含用户名、用户昵称、用户头像,这三个参数都和用户的信息有关,那么这里可以通过封装对象来缩减构造函数的参数数量。


data class UserInfo(val userName:String,val nickName:String,val avatarUrl:String)


class TopVC(
//用户信息
private val userInfo:UserInfo,
...){
...
}

通过将用户名、用户昵称、用户头像三个参数聚合为一个UserInfo对象来缩减TopVC的参数数量。有的人会说,这不扯犊子吗?我不是还得构建UserInfo这个对象吗?参数还是要传啊。这其实有本质上的区别,首先,当几个参数共同传递时,他们往往是作为某个具体概念的一部分了,有了概念就有了名字,理解上会更方便,同时拆分为小的对象,每个小的对象的构造函数的参数数量较少,减少因为传参顺序或者类型错误造成的风险问题。

当参数被多个组件使用时

如果构造函数的参数用来赋值给类中的成员变量,数据源被多个类或者多个层级的组件引用,那么应该考虑使用上下文或者环境对象作为参数,或者通过提供对上下文或者全局对象的访问方法,移除这部分参数,也可以用来缩减参数数量。


data class UserInfo(val userName:String,val nickName:String,val avatarUrl:String)

class TopVC(
//用户信息
private val userInfo:UserInfo,
...){
   private val env:Env=Env.getDefault()
}

总结

总结一下,无论什么函数,参数数量都不宜过多,正如《代码整洁之道》里提到的参数不易对付,他们有太多的概念性,只有足够特殊的理由才能使用三个以上的参数。

关注我的公众号:滑板上的老砒霜