引言
在面向对象编程中,参数的重要性不言而喻。经验丰富的程序员深知其益处:
- 参数是实现函数独立性和解耦的关键所在。通过将依赖的属性以参数的形式传递到函数内部,我们可以成功的将函数调用者与函数本身分离开来,从而提高了代码的灵活性和可维护性。
- 参数的存在大大提高了函数的复用性。通过传递不同的参数值,我们可以让同一个函数应对不同的场景和需求,从而提高了代码的复用效率。
然而,参数的使用也需要适度。过长的参数列表使人迷惑,还可能影响代码的可读性和可维护性。在实际业务开发中,我们可能会遇到这样的场景:
现在存在方法 Method ,此时它只有一个参数P1。
当对象 A 需要调用 Method 方法,但又希望一个值参与 Method 方法的逻辑计算时,就在 Method 方法的参数列表中新增参数 P2 。
当对象 B 需要调用 Method 方法,也希望一个值参与 Method 方法的逻辑计算时,就在 Method 方法的参数列表中新增参数 P3。
.....
每有一个新的对象需要调用 Method 方法,且都需要传入自身的一种依赖属性时,都会增加 Method 方法的参数列表长度。
虽然上述办法在一定程度上解决了业务问题,但也带来了潜在风险,为后期拓展和维护此函数时埋下隐患。所以这就需要我们权衡参数列表的长度,为后期维护和拓展作考虑。
今天,我通过阅读《重构》这本书,为参数列表长度的相关问题,提供一些权衡的参考:
- 参数列表过长的信号是什么?如何判断这是不是「过长参数列表」?
- 「过长参数列表」造成了什么问题?我们为什么要重构它?
- 「过长参数列表」有没有什么通用的解决方案?如何重构它?
特征
其实对于"参数列表是否过长"这个问题,业界内并没有统一答案,但也有几个参考标准:
- 通过参数的个数判断是否过长。一般来说,参数的合理个数应该在3 - 5个,超过8个就应该重构。
- 标记参数过多。 业界把"用作函数内部逻辑分支的判断条件型参数"称为标记参数,如布尔值、枚举。标记参数通常会降低函数的可读性,并增加函数的复杂度。
为什么要重构?
- 代码可读性差。调用者为了使用此函数,必须知晓每一个参数的含义和作用,参数越多,阅读时间越长,理解成本越大。
- 代码复杂性高。过长的参数列表,意味着函数内部的逻辑也十分复杂,包括但不限于逻辑分支、数值计算、对象操作,这都提高了代码复杂性,不利于后期维护。
- 代码可维护性低。如果需要改造包含了过长参数列表异味的函数,开发人员需要搞清楚每个参数的作用,而且还要特别注意每个参数之间是否存在依赖关系。
如何重构?
以查询替代参数
所谓"查询",就是把这个参数作为其他函数的返回值,并在函数内调用获取,而非由调用者传递。
- 优点:
-
- 避免过长参数列表。
- 降低调用难度。
- 缺点:
-
- 与其他函数或者类耦合,增强与外部的依赖关系
arduino
public int GetEmployeeVocation(int level, int serviceYears)
{
int result = 0;
...
...
return result;
}
ini
public int GetEmployeeVocation(int employeeId)
{
int result = 0;
int level = QueryLevel(employeeId);
int serviceYears = QueryServiceYears(employeeId);
...
...
return result;
}
原函数意图通过两个参数,计算该员工的年假天数。
但该函数的功能单一、意图清晰,可以通过传入员工编号,在函数内部"查询"获得员工的职级和司龄,并参与计算,最终返回年假天数,无需调用者传参。
保持对象完整
arduino
public void Alert(int low, int high)
{
if(!plan.WithinRange(low, high)
{
alerts("room temperature is out of range");
}
}
int low = room.DayTemperatureRange.Low;
int high = room.DayTemperatureRange.High;
Alert(low, high);//调用
scss
public void Alert(DayTemperatureRange temperatureRange)
{
if(!plan.WithinRange(temperatureRange.Low, temperatureRange.High)
{
alerts("room temperature is out of range");
}
}
Alert(room.DayTemperatureRange);//调用
在上述示例中,原函数在内部使用 plan 对象判断房间温度是否在允许范围内。若否,则警告。
不合理的地方在于,外部调用者需要将房间的温度范围取出,并赋值给 low 和 high ,再作为参数传给 Alert 函数。
为了减少参数列表的长度,我们可以将整个 DayTemperatureRange 对象作为参数传递,在函数内部取用。这样做可以把原来的两个参数减少到一个,也方便调用者使用。
引入参数对象
arduino
public void AmountInvoiced(DateTime startDate, DateTime endDate) {...}
public void AmountReceived(DateTime startDate, DateTime endDate) {...}
public void AmountOverdue(DateTime startDate, DateTime endDate) {...}
arduino
public class DateRange
{
public DateTime startDate;
public DateTime endDate;
}
public void AmountInvoiced(DateRange range) {...}
public void AmountReceived(DateRange range) {...}
public void AmountOverdue(DateRange range) {...}
在示例代码中,有三个函数都在使用这两个参数:startDate 、 endDate 。观察后发现,这三个函数的参数类型相同、个数相同,甚至连参数名称都相同。
所以组织一个新的参数对象 DateRange ,能够显著的缩短函数的参数列表,且在本项重构过程中,能够发现这些使用 DateRange 的函数的共同行为,更催生了代码在深层次的改变。
移除标记参数
首先让我们看一些常见的标记参数:
arduino
public void BookConcert(Customer customer, bool IsVip)
{
if (IsVip)
{
...
}
else
{
...
}
}
arduino
public void BookConcert(Customer customer, CustomerType customerType)
{
switch (customerType)
{
case CustomerType.Vip:
{
...
break;
}
case CustomerType.Normal:
{
...
break;
}
}
}
上述两个示例代码中,函数都包含了一个额外的参数:示例代码一的 IsVip ,示例代码二的 customerType 。这个额外的参数仅仅用作函数内部的判断条件,并没有参与内部的运算。
如果参数仅仅作为函数内部逻辑的判断条件,那可以将此函数拆分为多个。这不仅缩短了参数列表的长度,还推进了代码向深层次进行重构。
arduino
public void BookConcert(Customer customer)
{
...//普通客户订座
}
public void VipBookConcert(Customer customer)
{
...//高级客户订座
}
在本例中,我们将订座这个函数分为了普通客户订座和高级客户订座,避免使用标记参数,简化了函数的参数列表。
函数组合成类
当一个参数被多个函数引用,我们考虑将其封装成类,如下场景:
现在需要在客户端输出一位用户的名称、年龄、性别信息。
scss
static void Main(string[] args)
{
Person someone = new Person();
someone.Init();
PrintPersonName(someone);
PrintPersonAge(someone);
PrintPersonGender(someone);
}
public static void PrintPersonName(Person someone)
{
Console.WriteLine(Person.Name);
}
public static void PrintPersonAge(Person someone)
{
Console.WriteLine(Person.Age);
}
public static void PrintPersonGender(Person someone)
{
Console.WriteLine(Person.Gender);
}
PrintPersonName 、 PrintPersonAge 、 PrintPersonGender 三个函数都相同,且都是从 someone 对象中取值并输出。
我们可以通过把三个函数封装到 Person 类的内部,来达到缩减参数列表长度的目的:
csharp
static void Main(string[] args)
{
Person someone = new Person();
someone.Init();
someone.PrintName();
someone.PrintAge();
someone.PrintGender();
}
public class Person
{
private string name;
public string Name => name;
private int age;
public int Age => age;
private SexType gender;
public SexType Gender => gender;
public void PrintName()
{
Console.WriteLine(name);
}
public void PrintAge()
{
Console.WriteLine(age);
}
public void PrintGender()
{
Console.WriteLine(gender);
}
}