坏味道——重复代码(Duplicate Code)
重复代码堪称为代码坏味道之首。消除重复代码总是有利无害的。
特征
两个代码片段看上去几乎一样。
问题原因
重复代码通常发生在多个程序员同时在同一程序的不同部分上工作时。由于他们正在处理不同的任务,他们可能不知道他们的同事已经写了类似的代码。
还有一种更隐晦的重复,特定部分的代码看上去不同但实际在做同一件事。这种重复代码往往难以找到和消除。
有时重复是有目的性的。当急于满足deadline,并且现有代码对于要交付的任务是“几乎正确的”时,新手程序员可能无法抵抗复制和粘贴相关代码的诱惑。在某些情况下,程序员只是太懒惰。
解决方法
- 同一个类的两个函数含有相同的表达式,这时可以采用
提炼函数(Extract Method)
提炼出重复的代码,然后让这两个地点都调用被提炼出来的那段代码。
- 如果两个互为兄弟的子类含有重复代码:
- 首先对两个类都运用
提炼函数(Extract Method)
,然后对被提炼出来的函数运用函数上移(Pull Up Method)
,将它推入超类。 - 如果重复代码在构造函数中,运用
构造函数本体上移(Pull Up Constructor Body)
。 - 如果重复代码只是相似但不是完全相同,运用
塑造模板函数(Form Template Method)
获得一个 模板方法模式(Template Method) 。 - 如果有些函数以不同的算法做相同的事,你可以选择其中较清晰地一个,并运用
替换算法(Substitute Algorithm)
将其他函数的算法替换掉。 - 如果两个毫不相关的类中有重复代码:
- 请尝试运用
提炼超类(Extract Superclass)
,以便为维护所有先前功能的这些类创建一个超类。 - 如果创建超类十分困难,可以在一个类中运用
提炼类(Extract Class)
,并在另一个类中使用这个新的组件。 - 如果存在大量的条件表达式,并且它们执行完全相同的代码(仅仅是它们的条件不同),可以运用
合并条件表达式(Consolidate Conditional Expression)
将这些操作合并为单个条件,并运用提炼函数(Extract Method)
将该条件放入一个名字容易理解的独立函数中。 - 如果条件表达式的所有分支都有部分相同的代码片段:可以运用
合并重复的条件片段(Consolidate Duplicate Conditional Fragments)
将它们都存在的代码片段置于条件表达式外部。
收益
- 合并重复代码会简化代码的结构,并减少代码量。
- 代码更简化、更易维护。
重构方法说明
提炼函数(Extract Method)
问题
你有一段代码可以组织在一起。
void printOwing() {
printBanner();
//print details
System.out.println("name: " + name);
System.out.println("amount: " + getOutstanding());
}
解决
移动这段代码到一个新的函数中,使用函数的调用来替代老代码。
void printOwing() {
printBanner();
printDetails(getOutstanding());
}
void printDetails(double outstanding) {
System.out.println("name: " + name);
System.out.println("amount: " + outstanding);
}
函数上移(Pull Up Method)
问题
有些函数,在各个子类中产生完全相同的结果。
解决
将该函数移至超类。
构造函数本体上移(Pull Up Constructor Body)
问题
你在各个子类中拥有一些构造函数,它们的本体几乎完全一致。
class Manager extends Employee {
public Manager(String name, String id, int grade) {
this.name = name;
this.id = id;
this.grade = grade;
}
//...
}
解决
在超类中新建一个构造函数,并在子类构造函数中调用它。
class Manager extends Employee {
public Manager(String name, String id, int grade) {
super(name, id);
this.grade = grade;
}
//...
}
塑造模板函数(Form Template Method)
问题
你有一些子类,其中相应的某些函数以相同的顺序执行类似的操作,但各个操作的细节上有所不同。
解决
将这些操作分别放进独立函数中,并保持它们都有相同的签名,于是原函数也就变得相同了。然后将原函数上移至超类。
注:这里只提到具体做法,建议了解一下模板方法设计模式。
替换算法(Substitute Algorithm)
问题
你想要把某个算法替换为另一个更清晰的算法。
String foundPerson(String[] people){
for (int i = 0; i < people.length; i++) {
if (people[i].equals("Don")){
return "Don";
}
if (people[i].equals("John")){
return "John";
}
if (people[i].equals("Kent")){
return "Kent";
}
}
return "";
}
解决
将函数本体替换为另一个算法。
String foundPerson(String[] people){
List candidates =
Arrays.asList(new String[] {"Don", "John", "Kent"});
for (int i=0; i < people.length; i++) {
if (candidates.contains(people[i])) {
return people[i];
}
}
return "";
}
提炼超类(Extract Superclass)
问题
两个类有相似特性。
解决
为这两个类建立一个超类,将相同特性移至超类。
提炼类(Extract Class)
问题
某个类做了不止一件事。
解决
建立一个新类,将相关的字段和函数从旧类搬移到新类。
合并条件表达式(Consolidate Conditional Expression)
问题
你有一系列条件分支,都得到相同结果。
double disabilityAmount() {
if (seniority < 2) {
return 0;
}
if (monthsDisabled > 12) {
return 0;
}
if (isPartTime) {
return 0;
}
// compute the disability amount
//...
}
解决
将这些条件分支合并为一个条件,并将这个条件提炼为一个独立函数。
double disabilityAmount() {
if (isNotEligableForDisability()) {
return 0;
}
// compute the disability amount
//...
}
合并重复的条件片段(Consolidate Duplicate Conditional Fragments)
问题
在条件表达式的每个分支上有着相同的一段代码。
if (isSpecialDeal()) {
total = price * 0.95;
send();
}
else {
total = price * 0.98;
send();
}
引申阅读
欢迎继续阅读 代码的症与药 系列文章。