6.9 替换算法(Substitute Algorithm)
你想要把某个算法替换为另一个更清晰的算法。
将函数本体替换为另一个算法。
String foundPersone(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 foundPersone(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 "";
}
动机
解决问题的办法有很多,我们采取的那种往往可能不是最简单的。所以,当我们发现做一件事有更清晰、更简单、更高效的方式,就应该去修改。
“重构”可以把复杂东西变得简单,但随着时间,对问题有了更深的理解,有可能你发现这种重构不太合适,此时就应该改变原有的重构结构——改成一种更简单的方式。
使用这项重构手法之前,务必确认自己已经尽可能分解了原先函数:替换一个庞大的算法很难,分解成小型函数,能够更好的进行算法替换工作。
做法
-
准备好另一个(替换用的)算法,确认它的确能正常编译。
-
针对现有测试,执行上述的新算法。如果结果与原本结果相同,重构结束。
-
如果测试结果不同于原先,在测试和调试过程中,以旧算法为参照去比较。
第七章 在对象之间搬移特性
单一职责:就一个类而言,应该仅有一个引起它变化的原因。
我们之前提到过一种坏味道,名字叫做发散式变化(3.5节),那里假设了一种情况:当A需求改变时,本类要修改abc三个方法,当B需求改变时,本类修改的却是def三个方法,这种情况下就形成了责任分散——事实上abc应该在一个类中,def在另一个类中更为合理。
但是,在设计过程中,没有谁能够保证一次性把类设计好,让其符合单一职责。还好,我们有重构,可以根据需求改变原先的设计。
通常情况,使用搬移函数(7.1 Move Method)和搬移字段(7.2 Move Field)就能解决大多数问题。
而二者的先后顺序,往往是先搬移字段,后搬移函数。
当一个类呈现出发散式变化(3.5节)这样的坏味道时,就该考虑按照那一节所讲的,用提炼类(7.3 Extract Class)的方式将一部分责任分离出去。
当多个类具有同一种责任(霰弹式修改(3.6节):修改一个需求,要同时动很多类的内容)时,就该用将类内联化(7.4 Inline Class)的方法把职责集中起来。
如果你不希望看到某两个类之间存在依赖,那么就可以运用隐藏委托关系(7.5 Hide Delegate)的方式为二者加一个中间人。
有时,过度的隐藏委托类会导致拥有者的接口经常变化,此时就该移除中间人(7.6 Remove Middle Man)。
本章最后两个重构我们之前介绍过:在不完美类库(3.19 Incomplete Library Class)中提到的那俩。
引入外加函数(7.7 Introduce Foreign Method)和引入本地扩展(7.8 Introduce Local Extension),这两个可以在类库的基础上增加方法,变得更符合我们的需求。
7.1 搬移函数(Move Method)
你的程序中,有个函数与其所驻类之外的另一个类进行更多交流:调用后者,或被后者调用。
在该函数最常引用的类中建立一个有着类似行为的新函数,然后把旧函数变成单纯的委托函数,或是直接用新函数替换旧函数的引用点。
class A{
void b(){
System.out.println("I'm B.");
}
}
class B{}
>>>
class A{}
class B{
void b(){
System.out.println("I'm B.");
}
}
动机
搬移函数是重构理论的支柱,它贯穿了整本书,贯穿了大多数重构方法的内部。
搬移的使用时机是:一个类有太多行为,或者一个类中的某个方法与另一个类太过亲密。
通过搬移函数,能让系统中的类更简单,这些类最终也将更干净利落地实现系统交付的任务。
如果一个方法中,使用另一个对象的次数比使用自己所驻对象的次数还多,就应该观察一下调用它的那个位置、以及它调用的方法,以及继承体系中它的覆写方法。然后,根据这个函数与哪个对象最密切,决定移动路径。
如果说不确定是否应该搬移函数,此时请暂时放下,因为不恰当地搬移会适得其反。
做法
- 检查源类中被源函数所使用的一切特性(包括字段和函数),考虑它们是否也该被搬移。
如果某个特性只在此方法中使用,那么就应该将它一并随着此方法搬移。如果此特性不止在此方法中调用,看看那些调用此特性的方法能不能也拐卖走。
-
检查源类的子类和超类,看看是否有该函数的其他声明。
-
在目标类中声明这个函数(别忘了给它起个好名字)。
-
将源函数的代码复制到搬移后的新函数中。调整新函数,让其能够运行。
如果新函数使用了源类中的特性,就得决定如何从新函数那里引用源对象。如果新函数所在类不适合引用源对象,那就可以给新函数加一个参数传递源对象。
如果源函数包含异常处理,就得判断逻辑上应该由哪个类来处理这一异常。如果应该由源类来负责,就该把异常处理留在原地。
-
编译新函数所在类,确保没有报错。
-
决定如何从源函数中引用目标对象。
如果有现成字段或方法能取得目标对象,那是极好的,如果没有,就该看看能否轻松建立一个这样的函数。如果还不行,就在源类中新建一个专门字段去保存目标对象。
- 修改源函数,让其成为一个纯委托函数。
纯委托函数:函数内部只有一句代码,那句代码就是调用方法的语句。
-
编译、测试。
-
决定是否删除旧函数。
如果经常在源对象中引用目标函数,那么将源对象作为委托函数更好一点,这样就保证了不会重复的写很多次引用目标函数的语句。
-
如果要移除源函数,记得查找所有调用点,然后替换掉。
-
再编译、再测试。
范例
用一个表示“账户”的Account类来说明:
class Account {
private AccountType type;
//超支天数
private int daysOverdrawn;
double overdraftCharge() {
//是否是保险
if (type.isPremium()) {
double result = 10;
if(daysOverdrawn > 7)
result += (daysOverdrawn - 7) * 0.85;
return result;
} else
return daysOverdrawn * 1.75;
}
double bankCharge() {
double result = 4.5;
if(daysOverdrawn>0)
result += overdraftCharge();
return result;
}
}
假设有几种新账户,每一种都有自己的“透支金额计费规则”。所以我希望将overdraftCharge()搬移到AccountType类去。