引言
在大学里,我牢记面向对象编程的三大特性:继承、多态和封装。其中,封装是指将特定的代码从其他代码片段中独立出来,封装成独立的函数或模块,以便代码的维护和复用。
然而在实际的业务开发中,出于各种原因,重复的代码依旧频繁出现在不同的业务模块中。当需要维护这部分代码时,我必须找到全部副本,并仔细对比其中差异,以防止漏改或错改,这种工作方式让我心力憔悴。
在阅读《重构》这本书后,我结合个人的工作经验进行了深入的反思,并写下了这篇博客。在这篇博客中,我将探讨以下三个问题:
- 什么是「重复代码」?如何识别并定位它?
- 为什么一定要修改「重复代码」?如果不进行修改,会带来哪些问题?
- 如何处理不同情况下的「重复代码」?
特征
最简单的情况
最容易识别的情况是,同一个类中存在相同的表达式。下面是一个简单的例子:
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);
}
}
为什么重构?
- 如果不修改,维护成本高,维护风险大。 维护人员在每次维护、修改该块代码时,必须找出全部副本,逐一修改。且在修改过程中,还必须仔细留意其中的差别,否则会漏改。
- 如果修改,便于维护,减少重复工作量。
如何重构?
提炼函数
- 背景:同一个模块中,存在相同的代码。
- 步骤:
-
- 将相同代码提炼为独立的函数。
- 将所有相同代码的副本,替换成提炼的那个函数。
- 详细步骤:
第一步
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;
}
}
移动语句后再提炼
- 背景:因为语句顺序不同,代码相似但不相同
- 步骤:
- 调整语句顺序,使其相同代码显而易见。
- 将相同的部分代码提炼为独立的函数。
- 替换所有相同代码的副本。
- 详细步骤:
第一步
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();
}
}
函数上移
「函数上移」的应用场景比较复杂,且可以搭配其他重构手法,所以需要具体情况具体分析,下面介绍三种常用的使用场景和使用步骤。
直接上移到父类中
- 背景:同一个父类的不同子类中,存在相同的代码。但相同代码中,无引用自身变量
- 步骤:
- 将相同的代码,上移到父类。
- 删除子类中存在的相同代码。
- 详细步骤:
kotlin
public class BaseClass
{
public void Method()//把重复代码上移到父类中,子类无需实现
{
//do something C
}
}
public class InheritorA : BaseClass
{
//删除了重复代码
}
public class InheritorB : BaseClass
{
//删除了重复代码
}
先函数参数化,再上移
- 背景:同一个父类的不同子类中,存在相同代码。相同代码中,有引用自身属性的情况
- 步骤:
- 先将引用的属性变成参数,也就是所谓的「函数参数化」。
- 再将函数上移至父类,这样即可避免在子类中重复实现相同功能函数。
- 详细步骤:
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
{
}
先字段上移,再函数上移
- 背景:同一个父类的不同子类中,存在相同代码。相同代码中,有引用自身属性的情况。且引用的自身属性,可以提炼为父类的基础属性。
- 步骤:
- 将引用的属性上移至父类,也就是所谓的「字段上移」。要求上移的字段,可以作为基类的基础属性。
- 再将函数上移至父类,这样即可避免在子类中重复实现相同功能函数。
- 详细步骤:
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
{
//子类的重复代码已经删除
}
使用模版方法
- 背景:同一个父类的不同子类中,存在相同代码。相同代码中,有引用自身属性的情况。且引用的自身属性,可以提炼为父类的基础属性。
- 步骤:
- 将引用的属性上移至父类,也就是所谓的「字段上移」。要求上移的字段,可以作为基类的基础属性。
- 在父类编写虚方法,规定通用的行为流程,这也就是所谓的「模板方法」。
- 最后交由子类实现自身的多态属性。
- 详细步骤:
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();
}
}