代码重构之[过长函数]

引言

优秀的函数通常保持简洁。

  1. 刻意控制函数长度有助于剔除不属于该功能模块的冗余代码,从而提升代码的可读性和健壮性。
  2. 短小且功能明确的函数,有助于提升命名的准确性。而好的命名可以协助读者快速理解其作用,而无需逐行查看代码,减少了理解成本。

有经验的程序员通常坚持每个函数的行数不超过50行,这种原则对我产生了深远的影响。

然而,理想与现实总是存在差距。尽管短函数的优点明显,但在实际项目中,我们仍然会遇到一些过长的函数。为了更好地处理这一问题,我阅读了《重构》一书,并总结了以下三个关键问题:

  1. 如何判断一个函数是否过长?
  2. 函数过长会造成什么负面影响?
  3. 如何修改和重构「过长函数」?

特征

暗中作祟的函数命名

《代码重构之[神秘命名]》中我们提到:为取名而付出的纠结,往往能够促使我们去修改设计上的缺陷。

如果一个函数的名称是 A ,但实际上包含了 A B 两部分功能,那剔除 B 这一部分代码,能够有效的精简函数长度。实际上,造成函数变得越来越长,越来越臃肿的原因,大多数情况下,就是因为函数的功能不够单一。

因此,函数功能不单一,不仅代表函数名称不够准确,也可能代表该函数的长度还可以再精简一些

观察上下文中的注释

我们遵循一条原则:每当感到需要使用注释来说明什么的时候,我们就应该把需要说明的代码部分写进一个独立的函数中,并以其用途(而非实现手法)命名。

因此,我们可以将函数中出现的注释,视为一种函数过长的信号


为什么重构?

  1. 长函数提高了理解成本,当需要维护长函数时,会花费更多的时间去定位需要修改的部分。而短函数更具阐释力
  2. 长函数的内部常常会杂糅许多计算功能,当我们修改其中一处代码时,还需要确保这不会影响到其他部分,如果上下文存在依赖关系,那修改后还会扩大测试范围。而短函数更易于维护

如何重构?

提炼函数

我们观察到下面示例代码中,存在一句"打印info中所需信息"的注释,这代表注释之下的代码,可能与注释之上的代码逻辑不属于一个逻辑模块,这是一个重构的信号。

scss 复制代码
public class Example
{
    public void PrintCharacterInfo()
    {
        PrintTitle("该角色信息如下:");

        var character = QueryCurCharacter((c) => c.state == CharacterState.Select);
        var characterPrintInfo = character.GetPrintInfo();
        //打印info中所需信息
        Console.WriteLine(characterPrintInfo.nickName);
        Console.WriteLine(characterPrintInfo.sex);
        Console.WriteLine(characterPrintInfo.skinName);
        Console.WriteLine(characterPrintInfo.hairColor);
    }
}
scss 复制代码
public class Example
{
    public void PrintCharacterInfo(Character character)
    {
        PrintTitle("该角色信息如下:");

        var character = QueryCurCharacter((c) => c.state == CharacterState.Select);
        var characterPrintInfo = character.GetPrintInfo();
        PrintInfo(characterPrintInfo);
    }

    public void PrintInfo(CharacterPrintInfo printInfo)
    {
        Console.WriteLine(characterPrintInfo.nickName);
        Console.WriteLine(characterPrintInfo.sex);
        Console.WriteLine(characterPrintInfo.skinName);
        Console.WriteLine(characterPrintInfo.hairColor);
    }
}

以查询替代临时变量,再提炼

下面的例子是原作者编写的示例代码(原著中将查询的过程封装成函数,但C#中有更好用的"属性")。使用"查询"替代"临时变量"最大的好处是:

  • 可以使"查询的过程"与原函数之间形成明显的边界。
  • "查询的过程"函数可以多次复用。

使用"临时变量"的场景是:那些只被使用一次,且后续逻辑不会再变更的变量。如果这个变量可能被使用多次,或者这个变量后续可能会被修改,则推荐你使用"查询函数"。

csharp 复制代码
public float GetFinalPrice()
{
    var basePrice = itemCount * itemPrice;
    if (basePrice > 1000)
    {
        return basePrice * 0.95f;
    }
    else
    {
        return basePrice * 0.98f;
    }
}
csharp 复制代码
public float BasePrice
{
    get => itemCount * itemPrice;
}

public float GetFinalPrice()
{
    if (BasePrice > 1000)
    {
        return BasePrice * 0.95f;
    }
    else
    {
        return BasePrice * 0.98f;
    }
}

以命令取代函数

"命令"的定义: 函数,不管是独立函数,还是以方法(method)形式附着在对象上的函数, 是程序设计的基本构造块。不过,将函数封装成自己的对象,有时也是一种有用的办法。这样的对象我称之为"命令对象"(command object),或者简称"命 令"(command)。这种对象大多只服务于单一函数,获得对该函数的请求,执行该函数,就是这种对象存在的意义。

以命令取代函数的好处是,我们可以将复杂的计算,以更加灵活的方式呈现出来,对于这个命令对象,我们甚至可以添加更多的相关操作,比如取消操作。特别是为了应对单元测试、多场景调用的情况,命令对象总是能够灵活的满足上述需求,将命令发起者和命令接收者分隔开。

但命令对象本身就是复杂的,编写一个命令对象的代码量远多于函数。在大多数情况下,能够使用函数尽量使用,只有在普通函数无法满足需求,且不得不使用命令对象,且需要将函数的发起方和接收方解耦时,我才会选择它。

下面是一个命令对象取代函数的简单例子:

csharp 复制代码
public float GetScore(Student stu, ScoreRule rule)
{
    var result = 0f;

    //复杂的计算
    
    return result;
}
csharp 复制代码
public float GetScore(Student stu, ScoreRule rule)
{
    return new StudentScore(stu, rule).Get();
}

public class StudentScore
{
    private Student stu;
    private ScoreRule rule;
    
    public StudentScore(Student stu, ScoreRule rule)
    {
        this.stu = stu;
        this.rule = rule;
    }

    public float Get()
    {
        //复杂的计算
    }
}

以多态取代分支表达式

多态是面向对象的基本特征之一,但这并不代表所有的分支表达式都应该用多态替代。如果你有如下两种情况,原作者推荐使用多态。

  1. 每个条件下的逻辑复杂。
  2. 在多个地方反复调用此分支表达式。

下面是一个非常典型的示例。当我们需要多个地方、多次调用 GetCarColor 方法时,可以将此拆分成不同的 Car 类,每个类都可以实现自己的 GetCarColor 方法

typescript 复制代码
public Color GetCarColor(CarType type)
{
    switch(type)
    {
        case CarType.XiaoMi:
        {
            return Color.Orange;
        }
        case CarType.BMW:
        {
            return Color.White;
        }
        case CarType.AuDi:
        {
            return Color.Black;
        }
    }
}
kotlin 复制代码
public interface Icolor
{
    Color GetColor(){}
}

public class XiaoMiCar : IColor
{
    public Color GetColor()
    {
        return Color.Orange;
    }
}

public class BMWCar : IColor
{
    public Color GetColor()
    {
        return Color.White;
    }
}

public class AuDiCar : IColor
{
    public Color GetColor()
    {
        returnColor.Black;
    }
}

拆分循环

拆分循环的意义在于,将两段不同的逻辑在形式上拆分开。原作者认为,一个循环只为了做一件事,这样后来的读者能够很快速的、清晰的了解到这段循环的作用。

而且原作者还提到了,如果觉得拆分循环会对性能上造成压力,那么就在循环被拆分后,再去做性能优化,这是两件事,不能一起做。

下面是一个简单例子:

ini 复制代码
public void UpdateStudentInfos()
{
    for	(int i = 0; i < studentList.Count; i++)
    {
        UpdatePersonInfo(studentList[i].personInfo);
        UpdateScoreInfo(studentList[i].scoreInfo);
    }
}
ini 复制代码
public void UpdateStudentInfos()
{
    for	(int i = 0; i < studentList.Count; i++)
    {
        UpdatePersonInfo(studentList[i].personInfo);
    }

    for	(int i = 0; i < studentList.Count; i++)
    {
        UpdateScoreInfo(studentList[i].scoreInfo);
    }
}
相关推荐
非知名程序员2 天前
让JDK Record类拯救你的超长参数列表和臃肿Context对象
代码规范
SUN14862011818803 天前
JavaScript编码规范
javascript·代码规范
古力德3 天前
代码重构之[重复代码]
代码规范
古力德3 天前
代码重构之[神秘命名]
代码规范
围巾哥萧尘5 天前
「MCP建模操作」使用Blender MCP与Cursor完成3D建模的探索之旅🧣
数据库·代码规范
政采云技术5 天前
为什么我们要删掉 100% 的 useEffect(二)
前端·react.js·代码规范
政采云技术5 天前
为什么我们要删掉 100% 的 useEffect(一)
前端·react.js·代码规范
独立开阀者_FwtCoder9 天前
大模型私人定制:短短几行代码微调构建属于你的人工智能大模型(使用unsloth微调DeepSeek-r1大模型)
程序员·架构·代码规范
啾啾Fun9 天前
[代码规范]1_良好的命名规范能减轻工作负担
代码规范·命名规范·java命名规范·长命名方案