代码重构之[重复代码]

引言

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

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

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

  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();
    }
}
相关推荐
To_OC1 天前
万字解析《JS 语言精粹》之第五章:继承 5 大核心精髓(JS 原型核心)
前端·javascript·代码规范
Coffeeee1 天前
闲聊几句,Android老哥们,你们多久没做技改需求了
android·程序员·代码规范
饼干哥哥1 天前
扣子3.0测评:我让 Codex 和 Claude Code 住同一个桌面,结果它们打架了!
人工智能·开源·代码规范
码哥字节3 天前
为什么 Claude Code 读你的代码库,光靠 embedding 根本不够?
claude·代码规范
kisshyshy5 天前
从递归到迭代,一文吃透二叉树的核心知识与 JavaScript 实现
javascript·算法·代码规范
用户6919026813399 天前
Vibe Coding 开发项目的基本范式
人工智能·设计模式·代码规范
Cosolar9 天前
藏在 Claude Code 里的极致浪漫:完整 187 条 Spinner Verbs 全收录
后端·程序员·代码规范
Mickey86110 天前
MCP 加持下的零代码逆向:全自动化绕过 APP 验签与加密实战
代码规范
专注VB编程开发20年13 天前
WebView2 + HostObject 架构的核心痛点 ——强耦合、同步阻塞、异常连锁、内核绑定
代码规范