代码重构之[过长参数列表]

引言

在面向对象编程中,参数的重要性不言而喻。经验丰富的程序员深知其益处:

  1. 参数是实现函数独立性和解耦的关键所在。通过将依赖的属性以参数的形式传递到函数内部,我们可以成功的将函数调用者与函数本身分离开来,从而提高了代码的灵活性和可维护性。
  2. 参数的存在大大提高了函数的复用性。通过传递不同的参数值,我们可以让同一个函数应对不同的场景和需求,从而提高了代码的复用效率。

然而,参数的使用也需要适度。过长的参数列表使人迷惑,还可能影响代码的可读性和可维护性。在实际业务开发中,我们可能会遇到这样的场景:

现在存在方法 Method ,此时它只有一个参数P1。

当对象 A 需要调用 Method 方法,但又希望一个值参与 Method 方法的逻辑计算时,就在 Method 方法的参数列表中新增参数 P2 。

当对象 B 需要调用 Method 方法,也希望一个值参与 Method 方法的逻辑计算时,就在 Method 方法的参数列表中新增参数 P3。

.....

每有一个新的对象需要调用 Method 方法,且都需要传入自身的一种依赖属性时,都会增加 Method 方法的参数列表长度。

虽然上述办法在一定程度上解决了业务问题,但也带来了潜在风险,为后期拓展和维护此函数时埋下隐患。所以这就需要我们权衡参数列表的长度,为后期维护和拓展作考虑。

今天,我通过阅读《重构》这本书,为参数列表长度的相关问题,提供一些权衡的参考:

  1. 参数列表过长的信号是什么?如何判断这是不是「过长参数列表」?
  2. 「过长参数列表」造成了什么问题?我们为什么要重构它?
  3. 「过长参数列表」有没有什么通用的解决方案?如何重构它?

特征

其实对于"参数列表是否过长"这个问题,业界内并没有统一答案,但也有几个参考标准:

  1. 通过参数的个数判断是否过长。一般来说,参数的合理个数应该在3 - 5个,超过8个就应该重构。
  2. 标记参数过多。 业界把"用作函数内部逻辑分支的判断条件型参数"称为标记参数,如布尔值、枚举。标记参数通常会降低函数的可读性,并增加函数的复杂度。

为什么要重构?

  1. 代码可读性差。调用者为了使用此函数,必须知晓每一个参数的含义和作用,参数越多,阅读时间越长,理解成本越大。
  2. 代码复杂性高。过长的参数列表,意味着函数内部的逻辑也十分复杂,包括但不限于逻辑分支、数值计算、对象操作,这都提高了代码复杂性,不利于后期维护。
  3. 代码可维护性低。如果需要改造包含了过长参数列表异味的函数,开发人员需要搞清楚每个参数的作用,而且还要特别注意每个参数之间是否存在依赖关系。

如何重构?

以查询替代参数

所谓"查询",就是把这个参数作为其他函数的返回值,并在函数内调用获取,而非由调用者传递。

  • 优点:
    • 避免过长参数列表。
    • 降低调用难度。
  • 缺点:
    • 与其他函数或者类耦合,增强与外部的依赖关系
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);
    }
}

相关推荐
方圆想当图灵10 小时前
从 Java 到 Go:面向对象的巨人与云原生的轻骑兵
后端·代码规范
程序员JerrySUN14 小时前
设计模式 Day 2:工厂方法模式(Factory Method Pattern)详解
设计模式·工厂方法模式
牵牛老人17 小时前
C++设计模式-迭代器模式:从基本介绍,内部原理、应用场景、使用方法,常见问题和解决方案进行深度解析
c++·设计模式·迭代器模式
诺亚凹凸曼17 小时前
23种设计模式-结构型模式-组合
设计模式
诺亚凹凸曼17 小时前
23种设计模式-结构型模式-桥接器
android·java·设计模式
却尘21 小时前
跨域资源共享(CORS)完全指南:从同源策略到解决方案 (1)
前端·设计模式
coderzpw1 天前
设计模式中的“万能转换器”——适配器模式
设计模式·适配器模式
三金C_C1 天前
单例模式解析
单例模式·设计模式·线程锁
ShareBeHappy_Qin1 天前
设计模式——设计模式理念
java·设计模式
木子庆五1 天前
Android设计模式之代理模式
android·设计模式·代理模式