重构 改善既有代码的设计—— 重新组织方法

一.Extract Method(提炼方法)

1.动机:如果函数过长或代码段需要注释才能理解,就将这段代码放到独立函数中;有几个原因造成我喜欢简短而命名良好的的方法:

A.函数粒度小,复用几率高

B.函数粒度小,复写容易

C.函数粒度小,使高层函数读起来向一系列注释

   常常有人在问,一个方法长度多长才算合适。在我看来,方法多长不是问题,关键是方法名和方法体之间的语义距离。如果提炼可以强化方法的清晰度,即使提炼出来的方法的      方法名比方法体还长也无所谓。


2.做法

A.创造新的目标函数,根据函数功能命名;即使提炼的代码很简单, 只要目标函数的名称能更好的昭示代码意图,也应该提炼它。如果你想不出一个更有意义的名称,那就别动它;

B.将提炼的代码从源函数中复制到目标函数;

C.仔细检查提炼出的代码,查看其中是否引用了作用域仅限于源函数的变量(包括局部变量和源函数参数);

D.检查是否有仅用于被提炼代码段的临时变量,如果有,在目标函数中将他们声明为临时变量;

E.检查被提炼函数,查看是否有局部变量被其改变;如果有一个临时变量被改变,看看是否可以将被提炼代码段处理成查询,将返回值赋给相关变量;如果有多个临时变量被改变,就需要先使用Split Temporary Variable,然后再提炼;也可以先使用Replace Temp with Query消灭临时变量;

F.将被提炼代码段中需要的变量当作参数传给目标函数;

G.处理完所有变量后,编译;

H.在源函数中,将被提炼代码段替换成目标函数的引用;如果你将被提炼代码段的任何变量都放到目标函数中,请检查它们原来的声明是否还在,如果在的话,可以删除了;

I.编译,测试;


3.范例 

A无局部变量

private void printOwing(Enumeration enumeration){
    double outStanding = 0.0;

    System.out.println("*****************************");
    System.out.println("*******Custorm Owes**********");
    System.out.println("*****************************");

    while (enumeration.hasMoreElements()){
        Order order = (Order)enumeration.nextElement();
        outStanding += order.getNum();
    }

    System.out.println("name:" + name);
    System.out.println("outStanding :" + outStanding);
}

提炼打印横幅的代码段,只要剪切复制即可:

private void printOwing(Enumeration enumeration){
    double outStanding = 0.0;

    printBanner();
    
    while (enumeration.hasMoreElements()){
        Order order = (Order)enumeration.nextElement();
        outStanding += order.getNum();
    }

    System.out.println("name:" + name);
    System.out.println("outStanding :" + outStanding);
}


private void printBanner(){
    System.out.println("*****************************");
    System.out.println("*******Custorm Owes**********");
    System.out.println("*****************************");
}
B.有局部变量;问题点在意源函数的参数和源函数声明的临时变量,局部变量的作用域仅限于源函数,所以提炼代码段时需要花功夫处理这些临时变量;局部变量最简单的情况是被提炼代码只是读取这些值,而不用修改它们,这种情况下可以将局部变量当作参数传递给目标函数;

private void printOwing(Enumeration enumeration){
    double outStanding = 0.0;

    System.out.println("*****************************");
    System.out.println("*******Custorm Owes**********");
    System.out.println("*****************************");

    while (enumeration.hasMoreElements()){
        Order order = (Order)enumeration.nextElement();
        outStanding += order.getNum();
    }

    System.out.println("name:" + name);
    System.out.println("outStanding :" + outStanding);
}
将打印详细信息的代码段提炼到目标函数中:


private void printOwing(Enumeration enumeration){
    double outStanding = 0.0;
    printBanner();
    while (enumeration.hasMoreElements()){
        Order order = (Order)enumeration.nextElement();
        outStanding += order.getNum();
    }
    printDetail(outStanding);
}

private void printDetail(double outStanding){
    System.out.println("name:" + name);
    System.out.println("outStanding :" + outStanding);
}
 
C.对局部变量赋值 

如果被提炼的代码段对局部变量赋值,就略微复杂;此时可分两种情况:1.被赋值的临时变量只在目标函数中使用,此时可以将声明直接放到目标函数中。2.被提炼的代码段之外的代码也使用了这个变量,这也分两种情况:1.如果这个变量在被提炼代码段后使用,直接在代码段中修改就好了;如果被提炼的代码段后的代码也使用了这个变量,就需要让目标函数返回该变量修改后的值;代码如下:

private void printOwing(Enumeration enumeration){
    double outStanding = 0.0;
    printBanner();
    while (enumeration.hasMoreElements()){
        Order order = (Order)enumeration.nextElement();
        outStanding += order.getNum();
    }
    printDetail(outStanding);
}
现在把计算代码提炼出来:

 

private void printOwing(Enumeration enumeration){
    printBanner();
    double outStanding = getOutStanding(enumeration);
    printDetail(outStanding);
}

private double getOutStanding(Enumeration enumeration){
    double outStanding = 0.0;
    while (enumeration.hasMoreElements()){
        Order order = (Order)enumeration.nextElement();
        outStanding += order.getNum();
    }
    return outStanding;
}

二.Inline Method(内联方法)

一个函数的本体与名称同样清晰易懂,在函数调用点插入函数本体,然后移除该函数

private int getRating(){
    return (moreThanFiveLaterDeliveries()) ? 3:2;
}

private boolean moreThanFiveLaterDeliveries(){
    return numberOfLaterDeliveries > 5;
}

private int getRating(){
    return (numberOfLaterDeliveries > 5) ?3:2;
}

1.动机:

A.函数内部代码和函数名称同样清晰可读,此时应该去除函数名称,直接使用其中的代码;

B.有一群不甚合理的函数,此时可以把他们全部内联到一个大型函数中,再从中提炼出小函数;

C.如果使用了太多的间接层,使得系统中所有函数都似乎是对其他函数的简单委托,造成我在这些委托之间晕头转向,此时可以使用内联方法;当然间接层有其价值,但并    非所有间接层都有意义,找出那些无意义的,然后直接删除;

2.做法

A.检查函数,确定它不具有多态性(如果子类继承了该函数,就不要内联,因为子类无法继承一个不存在的函数);

B.找出该函数的所有调用点;

C.将这个函数的所有调用点替换成函数本体

D.编译、测试

E.删除该函数的定义;


三.Inline Temp(内联临时变量)

如果一个临时变量只被一个简单表达式赋值一次,而它妨碍了其他重构手法,将所有对该变量的操作,替换成对它赋值的那个表达式本身。

1.动机

A.Inline Temp多作为Replace Temp with Query的一部分使用的,所以真正的动机在后者;

B.Inline Temp单独使用的情况是,某个临时变量被赋予某个函数的返回值。一般来说,这个变量不会有什么影响,但是如果这个变量妨碍了其他重构手法,你就应该将之内联化;

2.做法

A.检查给临时变量赋值的语句,确保等号后边的赋值语句没有副作用;

B.如果这个临时变量没有被声明为final,那就声明为final,然后编译(可以检查该临时变量是否只被赋值一次);

C.找到该临时变量的所有引用点,替换成“为临时变量赋值的语句”表达式;

D.每次修改后,编译并测试;

E.修改完所有引用点后 ,删除该临时变量的声明和赋值语句;

F.编译、测试;


四.Replace Temp with Query(以查询取代临时变量)

程序以临时变量保存某一表达式的运算结果,将这个表达式提炼到独立函数中,把表达式的所有调用点换成独立函数的调用。

private int getResult(){
    int result = num * price;
    if (result > 10){
        return result + 20;
    }
    return result + 10;
}

private int getResult(){
    if (getCaculateResult() > 10){
        return getCaculateResult() + 20;
    }
    return getCaculateResult() + 10;
}

private int getCaculateResult(){
    return num * price;
}

1.动机

A.临时变量的问题在于作用域只在函数内,如果访问需要的临时变量,可能会驱使写出很长的代码;如果把临时变量换成查询,那么整个类中的所有函数都可以访问;

2.做法

A.找出只被赋值一次的临时变量(如果某个临时变量被多次赋值,考虑采用Split Temporary Variable,将之分割成多个临时变量

B.将该临时变量声明为final

C.编译(可检测该临时变量是否只被赋值一次)

D.将“对该变量赋值”的语句等号右侧提炼到独立函数中(1.首先将函数声明为private,以后如有需求再放开权限;2.确保提炼出来的函数无任何副作用,如果有的话,对它进行Separate Query from Modifier)

E.编译、测试

F.对该临时变量使用Inline Temp

3.范例

private double getPrice(){
    int basePrice = quantity * itemPrice;
    double discountFactor;
    if (basePrice > 100){
        discountFactor = 0.95;
    }else {
        discountFactor = 0.98;
    }
    return basePrice * discountFactor;
}

我希望将两个临时变量全都替换掉,当然每次一个。首先将变量声明为final,确认临时变量只被赋值一次(如果不是的话那说明当前的重构是不可用的)

private double getPrice(){
    final int basePrice = quantity * itemPrice;
    final double discountFactor;
    if (basePrice > 100){
        discountFactor = 0.95;
    }else {
        discountFactor = 0.98;
    }
    return basePrice * discountFactor;
}
 接下来替换临时变量,将等式右侧的表达式提炼出来;

private double getPrice(){
    final int basePrice = basePrice();
    final double discountFactor;
    if (basePrice > 100){
        discountFactor = 0.95;
    }else {
        discountFactor = 0.98;
    }
    return basePrice * discountFactor;
}

private int basePrice(){
    return quantity * itemPrice;
}
编译并测试,没问题的话将临时变量调用点全部换成目标函数

private double getPrice(){
    final double discountFactor;
    if (basePrice() > 100){
        discountFactor = 0.95;
    }else {
        discountFactor = 0.98;
    }
    return basePrice() * discountFactor;
}

然后以类似办法处理discountFactor

private double getDiscountFactor(){
    if (basePrice() > 100){
        return  0.95;
    }else {
        return  0.98;
    }
}
最后得到函数:

private double getPrice(){
    return basePrice() * getDiscountFactor();
}

五.Introduce Explaining Variable(引入解释性变量)

你有一个复杂的表达式,将该表达式(或其中的一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途。

if ((browser.toUpperCase().indexOf("MAC") == -1) && (platform.toUpperCase().indexOf("IE") == -1) && isWaitInitialized() && resize > 10) {
    // do something
}
改写成:

boolean isMacOS = (browser.toUpperCase().indexOf("MAC") == -1);
boolean isIEBrowsers = (platform.toUpperCase().indexOf("IE") == -1);
boolean isResized = (resize > 10);
if ( isMacOS && isIEBrowsers  && isWaitInitialized() && isResized) {
    // do something
}

1.动机

A.表达式可能非常难读和理解,换成临时变量可以有助于分解表达式;尤其是在条件逻辑中,以一个良好命名的临时变量来解释对应条件字句的意义。

B.Introduce Explaining Variable和Replace Temp with Query有异曲同工之妙。差别在于临时变量限制较大,只能在函数内部使用;而独立函数可以在整个类中使用。但是当Extract Method有困难的时候,使用Introduce Explaining Variable还是很方便的。


2.做法

A.声明一个final的临时变量,将待分解之复杂表达式或部分运算结果赋值给它;

B.将表达式中的“运算结果”替换成上述临时变量;

C.编译、测试;


六.Split Temporary Variable(分解临时变量)

程序中有某个临时变量被赋值超过一次,它既不是循环变量,也不用于收集计算结果,针对每次赋值,创建一个独立的、对应的临时变量。

float temp = 2 * (height + width);
System.out.println("temp = " + temp);
temp = height * width;
System.out.println("tem = " + temp);
修改成:

float temp = 2 * (height + width);
System.out.println("temp = " + temp);
float area = height * width;
System.out.println("area = " + area);


1.动机

A.临时变量有各种用途,某些变量自然会导致重复赋值。循环变量和结果收集变量就是两个典型例子:循环变量会随着每次循环而发生改变,结果收集变量负责将“通过整个函数运行”的结果收集起来。除了这两种情况外,还有很多临时变量用于保存一段冗余代码的计算结果,以便稍后使用。这种临时变量应该只被赋值一次。如果被赋值超过一次,就意味着在程序中它承担了多个责任,它就应该被替换成多个临时变量,每个变量只承担一个责任。同一个临时变量承担两件不同的事情,会使程序阅读者糊涂;

2.做法

A.在待分解的临时变量声明及第一次赋值处,修改其名称;

B.将新的临时变量声明为final;

C.以该临时变量的第二次赋值动作为界,修改此前对该临时变量的所有引用点,让他们信用新的临时变量;

D.在第二次赋值处,重新声明原先的那个临时变量;

E.编译、测试

F.重复上述过程,每次都在声明处修改变量名称,并修改下次赋值之前的引用点;


七.Remove Assignments To Parameters(移除对参数的赋值)

如果程序中对一个参数赋值,那么就用一个临时变量取代该参数的位置。

private void test(int count,double price){
    if (count > 10){
        price = price * 0.9;
    }
}
改成

private void test(int count,double price){
    double result = price;
    if (count > 10){
        result = result * 0.9;
    }
}

1.动机

A.避免降低代码清晰度,如果只把参数当作被传递进来的东西会清晰很多

B.混淆了java按值传递和引用传递,前者对参数的任何修改,都不会对调用段产生影响;

2.做法

A.建立一个临时变量,把待处理的参数值赋值给它;

B.以“对此参数赋值”为界,把界限之后对参数的所有引用点替换成对此临时变量的调用;

C.编译、测试


八.Replace Method with Method Object(以函数对象替代函数)

你有一个大型函数,由于局部变量过多使你无法采用Extract Method(提炼方法),将这个函数放到一个单独对象中,如此局部变量就变成了对象内的字段,然后你就可以在同一个对象中将这个大型函数分解成多个小型函数;


1.动机

A.对付局部变量,可以采用Inline Temp和Replace Temp with Method,但是如果函数过大,有时候这两种方式都解决不了问题。此时,应该考虑用对象的方式,这样可以把所有局部变量变成函数对象的变量,然后可以对此函数使用Extract Method方法提炼成一个个小函数;


2.做法

A.建立一个新类,根据待分解函数的用途为此对象命名;

B.在新类中建立一个final字段,用以保存原大型函数所在对象,称为源对象。同时,针对原函数的每个参数和临时变量,在新类中建立一个对应字段保存;

C.在新类中建立一个构造函数,用以接受原函数所在对象及所有参数;

D.在新类中建立一个compute()函数;

E.将原函数的所有代码复制到compute中,如果需要调用源对象的任何参数,请通过源对象调用;

F.编译

G.将就函数的函数本体替换成这样一句话“创建上述新类的一个新对象,而后调用其中的compute()函数”


九.Substitute Algorithm(替换算法)

你要想把某个算法替换成另外一个算法,将函数本体替换为另外一个方法

private String findPerson(String[] persons){
    for (int i = 0;i < persons.length;i++){
        if(persons[i].equals("张三")){
            return "张三";
        }else if (persons[i].equals("李四")){
            return "李四";
        }else if (persons[i].equals("王五")){
            return "王五";
        }
    }
    return "";
}
替换成

private String findPerson(String[] persons){
    List candidates = Arrays.asList(new String[]{"张三","李四","王五"});
    for (int i = 0;i < persons.length;i++){
       if(candidates.contains(persons[i])){
           return persons[i];
       }
    }
    return "";
}

1.动机

A.解决问题用多种方法,总有某些方法会比较容易,当你觉得当前的算法逻辑有更好的取代方案时,那你就替换上就好了

2.做法

A.准备好另外一个算法,并测试通过编译

B.针对现有测试,替换上述新算法,如果结果与之前一致,重构结束;如果不一致,在测试和调试过程中,以就算法为比较参考标准;



 

第1章 重构,第一个案例 1 1.1 起点 1 1.2 重构的第一步 7 1.3 分解并重组statement() 8 1.4 运用多态取代与价格相关的条件逻辑 34 1.5 结语 52 第2章 重构原则 53 2.1 何谓重构 53 2.2 为何重构 55 2.3 何时重构 57 2.4 怎么对经理说 60 2.5 重构的难题 62 2.6 重构设计 66 2.7 重构与性能 69 2.8 重构起源何处 71 第3章 代码的坏味道 75 3.1 Duplicated Code(重复代码) 76 3.2 Long Method(过长函数) 76 3.3 Large Class(过大的类) 78 3.4 Long Parameter List(过长参数列) 78 3.5 Divergent Change(发散式变化) 79 3.6 Shotgun Surgery(霰弹式修改) 80 3.7 Feature Envy(依恋情结) 80 3.8 Data Clumps(数据泥团) 81 3.9 Primitive Obsession(基本类型偏执) 81 3.10 Switch Statements(switch惊悚现身) 82 3.11 Parallel InheritanceHierarchies(平行继承体系) 83 3.12 Lazy Class(冗赘类) 83 3.13 Speculative Generality(夸夸其谈未来性) 83 3.14 Temporary Field(令人迷惑的暂时字段) 84 3.15 Message Chains(过度耦合的消息链) 84 3.16 Middle Man(中间人) 85 3.17 Inappropriate Intimacy(狎昵关系) 85 3.18 Alternative Classes with Different Interfaces(异曲同工的类) 85 3.19 Incomplete Library Class(不完美的库类) 86 3.20 Data Class(纯稚的数据类) 86 3.21 Refused Bequest(被拒绝的遗赠) 87 3.22 Comments(过多的注释) 87 第4章 构筑测试体系 89 4.1 自测试代码的价值 89 4.2 JUnit测试框架 91 4.3 添加更多测试 97 第5章 重构列表 103 5.1 重构的记录格式 103 5.2 寻找引用点 105 5.3 这些重构手法有多成熟 106 第6章 重新组织函数 109 6.1 Extract Method(提炼函数) 110 6.2 Inline Method(内联函数) 117 6.3 Inline Temp(内联临时变量) 119 6.4 Replace Temp with Query(以查询取代临时变量) 120 6.5 Introduce Explaining Variable(引入解释性变量) 124 6.6 Split Temporary Variable(分解临时变量) 128 6.7 Remove Assignments to Parameters(移除对参数的赋值) 131 6.8 Replace Method with Method Object(以函数对象取代函数) 135 6.9 Substitute Algorithm(替换算法) 139 第7章 在对象之间搬移特性 141 7.1 Move Method(搬移函数) 142 7.2 Move Field(搬移字段) 146 7.3 Extract Class(提炼类) 149 7.4 Inline Class(将类内联化) 154 7.5 Hide Delegate(隐藏“委托关系”) 157 7.6 Remove Middle Man(移除中间人) 160 7.7 Introduce Foreign Method(引入外加函数) 162 7.8 Introduce Local Extension(引入本地扩展) 164 第8章 重新组织数据 169 8.1 Self Encapsulate Field(自封装字段) 171 8.2 Replace Data Value with Object(以对象取代数据值) 175 8.3 Change Value to Reference(将值对象改为引用对象) 179 8.4 Change Reference to Value(将引用对象改为值对象) 183 8.5 Replace Array with Object(以对象取代数组) 186 8.6 Duplicate Observed Data(复制“被监视数据”) 189 8.7 Change Unidirectional Association to Bidirectional(将单向关联改为双向关联) 197 8.8 Change Bidirectional Association to Unidirectional(将双向关联改为单向关联) 200 8.9 Replace Magic Number with Symbolic Constant(以字面常量取代魔法数) 204 8.10 Encapsulate Field(封装字段) 206 8.11 Encapsulate Collection(封装集合) 208 8.12 Replace Record with Data Class(以数据类取代记录) 217 8.13 Replace Type Code with Class(以类取代类型码) 218 8.14 Replace Type Code with Subclasses(以子类取代类型码) 223 8.15 Replace Type Code with State/Strategy(以State/Strategy取代类型码) 227 8.16 Replace Subclass with Fields(以字段取代子类) 232 第9章 简化条件表达式 237 9.1 Decompose Conditional(分解条件表达式) 238 9.2 Consolidate Conditional Expression(合并条件表达式) 240 9.3 Consolidate Duplicate Conditional Fragments(合并重复的条件片段) 243 9.4 Remove Control Flag(移除控制标记) 245 9.5 Replace Nested Conditional with Guard Clauses(以卫语句取代嵌套条件表达式) 250 9.6 Replace Conditional with Polymorphism(以多态取代条件表达式) 255 9.7 Introduce Null Object(引入Null对象) 260 9.8 Introduce Assertion(引入断言) 267 第10章 简化函数调用 271 10.1 Rename Method(函数改名) 273 10.2 Add Parameter(添加参数) 275 10.3 Remove Parameter(移除参数) 277 10.4 Separate Query from Modifier(将查询函数和修改函数分离) 279 10.5 Parameterize Method(令函数携带参数) 283 10.6 Replace Parameter with Explicit Methods(以明确函数取代参数) 285 10.7 Preserve Whole Object(保持对象完整) 288 10.8 Replace Parameter with Methods(以函数取代参数) 292 10.9 Introduce Parameter Object(引入参数对象) 295 10.10 Remove Setting Method(移除设值函数) 300 10.11 Hide Method(隐藏函数) 303 10.12 Replace Constructor with Factory Method(以工厂函数取代构造函数) 304 10.13 Encapsulate Downcast(封装向下转型) 308 10.14 Replace Error Code with Exception(以异常取代错误码) 310 10.15 Replace Exception with Test(以测试取代异常) 315 第11章 处理概括关系 319 11.1 Pull Up Field(字段上移) 320 11.2 Pull Up Method(函数上移) 322 11.3 Pull Up Constructor Body(构造函数本体上移) 325 11.4 Push Down Method(函数下移) 328 11.5 Push Down Field(字段下移) 329 11.6 Extract Subclass(提炼子类) 330 11.7 Extract Superclass(提炼超类) 336 11.8 Extract Interface(提炼接口) 341 11.9 Collapse Hierarchy(折叠继承体系) 344 11.10 Form Tem Plate Method(塑造模板函数) 345 11.11 Replace Inheritance with Delegation(以委托取代继承) 352 11.12 Replace Delegation with Inheritance(以继承取代委托) 355 第12章 大型重构 359 12.1 Tease Apart Inheritance(梳理并分解继承体系) 362 12.2 Convert Procedural Design to Objects(将过程化设计转化为对象设计) 368 12.3 Separate Domain from Presentation(将领域和表述/显示分离) 370 12.4 Extract Hierarchy(提炼继承体系) 375 第13章 重构,复用与现实 379 13.1 现实的检验 380 13.2 为什么开发者不愿意重构他们的程序 381 13.3 再论现实的检验 394 13.4 重构的资源和参考资料 394 13.5 从重构联想到软件复用和技术传播 395 13.6 小结 397 13.7 参考文献 397 第14章 重构工具 401 14.1 使用工具进行重构 401 14.2 重构工具的技术标准 403 14.3 重构工具的实用标准 405 14.4 小结 407 第15章 总结 409
一直很喜欢重构这本书,但是由于自己记性不太好,书看过之后其中的方法总是记不住,于是想如果有电子版的重构书就好了,工作中遇到重构的问题可以随时打开查阅。在网上搜索了许久,发现重构这本书有英文chm版本的,而中文版的电子书只有扫描的PDF版本,用起来非常不方便。于是萌生想做一本重构工具书的想法,本来打算自己重新重构书的内容再整理归类一下,后来发现原书的目录编排就很适合做工具书,包括坏味道分类,重构手法归类等,都有了一个比较系统的整理。因此,我利用空余时间制作了这样的一本中文的chm版重构,希望对大家有所帮助,也算对中国软件业做出一点小小的贡献。 本书基本上是取自”重构”中文版一书的内容,但格式上参照的是chm英文版的格式,还有一些格式小修改,比如第一章的重构前后代码对比。因为时间匆促,个人能力有限,本书难免存在一些缺漏,如果大家发现有问题,随时可以给我发邮件,我会尽快更新错误的内容。 最后再次感谢几位大师 Martin Fowler、Kent Beck等,还有翻译的侯捷和熊节先生,为我们带来这么精彩的一本书。谢谢。 免责声明:本书仅供个人学习研究之用,不得用于任何商业目的,不得以任何方式修改本作品,基于此产生的法律责任本人不承担任何连带责任。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值