代码重构之[重复代码]

引言

在大学里,我牢记面向对象编程的三大特性:继承、多态和封装。其中,封装是指将特定的代码从其他代码片段中独立出来,封装成独立的函数或模块,以便代码的维护和复用。

然而在实际的业务开发中,出于各种原因,重复的代码依旧频繁出现在不同的业务模块中。当需要维护这部分代码时,我必须找到全部副本,并仔细对比其中差异,以防止漏改或错改,这种工作方式让我心力憔悴。

在阅读《重构》这本书后,我结合个人的工作经验进行了深入的反思,并写下了这篇博客。在这篇博客中,我将探讨以下三个问题:

  1. 什么是「重复代码」?如何识别并定位它?
  2. 为什么一定要修改「重复代码」?如果不进行修改,会带来哪些问题?
  3. 如何处理不同情况下的「重复代码」?

特征

最简单的情况

最容易识别的情况是,同一个类中存在相同的表达式。下面是一个简单的例子:

csharp 复制代码
public class Demo
{
    public void MethodA()
    {
        if (A && B)//重复的判断条件
        {
            //do something
        }
    }

    public void MethodB()
    {
        if (A && B)//重复的判断条件
        {
            //do something a
        }
        else
        {
            //do something b
        }
    }
}

相似但不相同

项目中最常见的重复代码就是这种情况,代码组织顺序不同,但总体相似。如果不仔细观察,是很难发现其中重复的部分。

scss 复制代码
public class Demo
{
    public void MethodA()
    {
        DoSomethingA();
        DoSomethingB();
        DoSomethingC();
    }

    public void MethodB()
    {
        //MethodB的B、C部分与MethodA的B、C部分是相同的
        DoSomethingB();
        DoSomethingD();
        DoSomethingC();
    }

    public void MethodC()
    {
        //MethodC的B、C部分与MethodA的B、C部分是相同的
        DoSomethingB();
        DoSomethingA();
        DoSomethingC();
        DoSomethingF();
    }
}

兄弟子类有相同的代码

同一个父类下的多个子类,有相同的代码,这一点相较于前两点,是比较难察觉的。

无引用自身特性的情况

csharp 复制代码
public class BaseClass
{
}

public class InheritorA : BaseClass
{
    public void Method()//同 InheritorB 的 Function 函数作用是一致的
    {
        //do something C
    }
}

public class InheritorB : BaseClass
{
    public void Function()//同 InheritorA 的 Method 函数作用是一致的
    {
        //do something C
    }
}

有引用自身特性的情况

csharp 复制代码
public class BaseClass
{
}

public class InheritorA : BaseClass
{
    public string myName;
    
    public void Method()//同 InheritorB 的 Function 函数作用是一致的
    {
        Log(myName);
    }
}

public class InheritorB : BaseClass
{
    public string myName;
    
    public void Function()//同 InheritorA 的 Method 函数作用是一致的
    {
        Log(myName);
    }
}

为什么重构?

  1. 如果不修改,维护成本高,维护风险大。 维护人员在每次维护、修改该块代码时,必须找出全部副本,逐一修改。且在修改过程中,还必须仔细留意其中的差别,否则会漏改。
  2. 如果修改,便于维护,减少重复工作量。

如何重构?

提炼函数

  • 背景:同一个模块中,存在相同的代码。
  • 步骤:
    • 将相同代码提炼为独立的函数。
    • 将所有相同代码的副本,替换成提炼的那个函数。
  • 详细步骤:
第一步 复制代码
public class Demo
{
    public void MethodA()
    {
        if (A && B)
        {
            //do something
        }
    }

    public void MethodB()
    {
        if (A && B)
        {
            //do something a
        }
        else
        {
            //do something b
        }
    }

    public bool IsConditionOk()//提炼一个公用的函数
    {
        return A && B;
    }
}
第二步 复制代码
public class Demo
{
    public void MethodA()
    {
        if (IsConditionOk())//将原来的条件表达式替换为新的公用函数
        {
            //do something
        }
    }

    public void MethodB()
    {
        if (IsConditionOk())//将原来的条件表达式替换为新的公用函数
        {
            //do something a
        }
        else
        {
            //do something b
        }
    }

    public bool IsConditionOk()
    {
        return A && B;
    }
}

移动语句后再提炼

  • 背景:因为语句顺序不同,代码相似但不相同
  • 步骤:
  1. 调整语句顺序,使其相同代码显而易见。
  2. 将相同的部分代码提炼为独立的函数。
  3. 替换所有相同代码的副本。
  • 详细步骤:
第一步 复制代码
public class Demo
{
    public void MethodA()
    {
        DoSomethingA();
        DoSomethingB();
        DoSomethingC();
    }

    public void MethodB()
    {
        DoSomethingD();
        DoSomethingB();//经过移动语句后,重复代码就显而易见了。但在移动前,请确保移动后不会改变原有逻辑
        DoSomethingC();
    }

    public void MethodC()
    {
        DoSomethingA();
        DoSomethingB();//经过移动语句后,重复代码就显而易见了。但在移动前,请确保移动后不会改变原有逻辑
        DoSomethingC();
        DoSomethingF();
    }
}
第二步 复制代码
public class Demo
{
    public void MethodA()
    {
        DoSomethingA();
        DoSomethingB();
        DoSomethingC();
    }

    public void MethodB()
    {
        DoSomethingD();
        DoSomethingB();
        DoSomethingC();
    }

    public void MethodC()
    {
        DoSomethingA();
        DoSomethingB();
        DoSomethingC();
        DoSomethingF();
    }

    //提炼 B C 为新的函数
    public void DoBC()
    {
        DoSomethingB();
        DoSomethingC();
    }
}
第三步 复制代码
public class Demo
{
    public void MethodA()
    {
        DoSomethingA();
        DoBC();//替换
    }

    public void MethodB()
    {
        DoSomethingD();
        DoBC();//替换
    }

    public void MethodC()
    {
        DoSomethingA();
        DoBC();//替换
        DoSomethingF();
    }

    public void DoBC()
    {
        DoSomethingB();
        DoSomethingC();
    }
}

函数上移

「函数上移」的应用场景比较复杂,且可以搭配其他重构手法,所以需要具体情况具体分析,下面介绍三种常用的使用场景和使用步骤。

直接上移到父类中

  • 背景:同一个父类的不同子类中,存在相同的代码。但相同代码中,无引用自身变量
  • 步骤:
  1. 将相同的代码,上移到父类。
  2. 删除子类中存在的相同代码。
  • 详细步骤:
kotlin 复制代码
public class BaseClass
{
    public void Method()//把重复代码上移到父类中,子类无需实现
    {
        //do something C
    }
}

public class InheritorA : BaseClass
{
    //删除了重复代码
}

public class InheritorB : BaseClass
{
    //删除了重复代码
}

先函数参数化,再上移

  • 背景:同一个父类的不同子类中,存在相同代码。相同代码中,有引用自身属性的情况
  • 步骤:
  1. 先将引用的属性变成参数,也就是所谓的「函数参数化」。
  2. 再将函数上移至父类,这样即可避免在子类中重复实现相同功能函数。
  • 详细步骤:
csharp 复制代码
public class BaseClass
{
}

public class InheritorA : BaseClass
{
    public void Method(string name)//将myName字段替换为函数参数
    {
        Log(name);
    }
}

public class InheritorB : BaseClass
{
    public void Function(string name)//将myName字段替换为函数参数
    {
        Log(name);
    }
}
kotlin 复制代码
public class BaseClass
{
    public void Method(string name)//将子类的重复代码移动到父类中实现了
    {
        Log(name);
    }
}

public class InheritorA : BaseClass
{
}

public class InheritorB : BaseClass
{
}

先字段上移,再函数上移

  • 背景:同一个父类的不同子类中,存在相同代码。相同代码中,有引用自身属性的情况。且引用的自身属性,可以提炼为父类的基础属性。
  • 步骤:
  1. 将引用的属性上移至父类,也就是所谓的「字段上移」。要求上移的字段,可以作为基类的基础属性。
  2. 再将函数上移至父类,这样即可避免在子类中重复实现相同功能函数。
  • 详细步骤:
csharp 复制代码
public class BaseClass
{
    public string myName;//父类新增了 myName 字段
}

public class InheritorA : BaseClass
{
    //子类删除了 myName 字段
    
    public void Method()
    {
        Log(myName);
    }
}

public class InheritorB : BaseClass
{
    //子类删除了 myName 字段
    
    public void Function()
    {
        Log(myName);
    }
}
kotlin 复制代码
public class BaseClass
{
    public string myName;
    
    public void Method()//把子类的函数上移到了父类
    {
        Log(myName);
    }
}

public class InheritorA : BaseClass
{
    //子类的重复代码已经删除
}

public class InheritorB : BaseClass
{
    //子类的重复代码已经删除
}

使用模版方法

  • 背景:同一个父类的不同子类中,存在相同代码。相同代码中,有引用自身属性的情况。且引用的自身属性,可以提炼为父类的基础属性。
  • 步骤:
  1. 将引用的属性上移至父类,也就是所谓的「字段上移」。要求上移的字段,可以作为基类的基础属性。
  2. 在父类编写虚方法,规定通用的行为流程,这也就是所谓的「模板方法」。
  3. 最后交由子类实现自身的多态属性。
  • 详细步骤:
csharp 复制代码
public class BaseClass
{
    public string myName;//子类的字段放在了父类中
}

public class InheritorA : BaseClass
{
    //子类已经删除了 myName 字段
    
    public void Method()
    {
        Log(myName);
    }
}

public class InheritorB : BaseClass
{
    //子类已经删除了 myName 字段
    
    public void Function()
    {
        Log(myName);
    }
}
csharp 复制代码
public class BaseClass
{
    public string myName;
    
    public virtual void Method()//父类新增虚方法,供子类重写
    {
        Log(myName);
    }
}

public class InheritorA : BaseClass
{
    public void Method()
    {
        Log(myName);
    }
}

public class InheritorB : BaseClass
{
    public void Function()
    {
        Log(myName);
    }
}
csharp 复制代码
public class BaseClass
{
    public string myName;

    public virtual void Method()
    {
        Log(myName);
    }
}

public class InheritorA : BaseClass
{
    public override void Method()//细节交由子类实现
    {
        myName = "this is A";
        base.Method();
    }
}

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