重构之重新组织函数

@(常识)
[toc]
那有什么天生如此,只是我们天天坚持。
本篇文章主要讲解 《重构—-改善既有代码的设计》 这本书中的 第六章重新组织函数中 的知识点,
将现有的函数重新分解是 进行大型优雅重构的第一步!

内联函数

问题:一个函数的本体与名称同样清楚易懂
解决:在函数调用点插入函数本体,然后移除该函数

  1. //重构前
  2. public int getRating(){
  3. return (moreThanSixLateDeliveries()) ? 2 : 1;
  4. }
  5. boolean moreThanSixLateDeliveries(){
  6. return _numberOfLateDeliveries > 6;
  7. }
  1. //重构后
  2. public int getRating(){
  3. return (_numberOfLateDeliveries > 6) ? 2 : 1;
  4. }

动机

重构过程中经常会以简短的函数来表现动作意图,这样就使得代码更清晰易读。
但有时你会遇到某些函数,其内部代码和函数名称同样清晰易读。可能你重构了该函数,使得其内容和其名称变得同样清晰。果真如此,你就应该去掉这个函数,直接使用其中的代码。间接性可能会带来一些帮助,但是没有必要的间接性总是让人感觉不舒服。
还有一种情况是:你手上有一群组织不甚合理的函数。
你可以将它们都内联到一个大型函数中,再从中提炼出组织合理的小型函数。比起既要移动一个函数、又要移动它所调用的其它所有函数,将整个大型函数作为整体来移动会比较的简单。
如果你发现代码中使用了太多的间接层,使得系统中的所有函数都似乎只是对另一个函数的简单委托,造成对被些委托动作弄的晕头转向,这时通常也会使用内联函数。

做法

(1)检查函数,确定其不具有多态性。(如果子类继承了这个函数,就不要将此函数内联,因为子类无法复写一个根本不存在的函数)。
(2)找出这个函数的所有被调用点。
(3)将这个函数的所有被调用点都替换为函数本体。
(4)编译,测试。
(5)一切正常后,删除该函数的定义。
内联函数看起来似乎很简单。但情况往往那并非如此。对于递归调用、内联至另一个对象中而该对象并无提供访问函数……每一种情况都会很复杂。不介绍复杂情形是因为:如果你遇到了这样复杂的情形,那么就不该运用这种重构手法。

内联临时变量

问题:你有一个临时变量,只被一个简单表达式赋值一次,而它妨碍了其它的重构手法
解决:将所有对该变量的引用动作,替换为对它赋值的那个表达式自身。

  1. //重构前
  2. double basePrice = singleOrder.basePrice();
  3. return (basePrice > 1000)
  1. //重构后
  2. return (singleOrder.basePrice() > 1000)

动机

内联临时变量多数情况是作为“以查询取代临时变量”的一部分来进行使用的,而真正的动机是出现在“以查询取代临时变量”中。
唯一单独使用内联临时变量的情况是:你发现某个临时变量被赋予某个函数调用的返回值。一般来说,这样的临时变量是不会造成任何危害的,也可以放心地放在那儿。
但是,如果这个临时变量妨碍了其它的重构手法(例如提炼函数),你就应该将其内联化。

做法

(1)检查给临时变量赋值的语句,确保等号右边的表达式没有副作用。
(2)如果这个临时变量并未被声明为final,那就将它声明为final,然后编译。(这可以检查该临时变量是否真的只被赋值一次)
(3)找到该临时变量所有引用点,将它们替换为“为临时变量赋值”的表达式
(4)每次修改后,编译并测试。
(5)修改完后所有引用点后,删除该临时变量的声明和赋值语句。
(6)编译,测试

引入解释性变量

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

  1. //重构前
  2. if((platform.toUpperCase().indexOf("MAC") > -1) &&
  3. (browser.toUpperCase().indexOf("IE") > -1) &&
  4. wasInitialized() && resize > 0)
  5. {
  6. //do something
  7. }
  1. //重构后
  2. final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1;
  3. final boolean isIEBrowser = browser.toUpperCase().indexOf("IE") > -1;
  4. final boolean wasResize = resize > 0;
  5. if(isMacOs && isIEBrowser && wasInitialized() && wasResize){
  6. //do something
  7. }

动机

在某些情况下,表达式可能非常的复杂以至于难以阅读。这样,临时变量可以帮助你将表达式分解为比较容易管理的形式。
条件逻辑中,引入解释性变量就显得比较有价值:你可以用这项重构将每个子句提炼出来,以一个良好命名的临时变量来解释对应条件子句的意义。另一种可能的情况是,对于那些比较长的算法,可以运用临时变量来解释每一步运算的意义
本文的重构手法是比较常见的手法之一,但是对其的使用又不是那么的多。因为一般情况下,我们都可以使用提炼函数来解释一段代码的意义。毕竟临时变量只有在它所处的那个函数中才有意义,局限性较大,函数则可以在对象的整个生命周期中都有用,并且可被其它对象使用。但是,当局部变量使用提炼函数难以进行时,就可以尝试使用引入解释性变量。

做法

(1)声明一个final型的临时变量,将待分解之复杂表达式中的一部分动作的运算结果赋值给它。
(2)将表达式中的“运算结果”这一部分,替换为上述的临时变量。(如果被替换的这一部分在代码中重复出现,可以每次一个,逐一进行替换)
(3)编译,测试。
(4)重复上述过程,处理其它类似部分。

示例

  1. //重构前
  2. double price(){
  3. // 价格 = basePrice - quantity discount + shipping
  4. return _quantity * _itemPrice -
  5. Math.max(0, _quantity - 800) * _itemPrice * 0.15 +
  6. Math.min(_quantity * _itemPrice * 0.25, 100);
  7. }

这段代码还是比较简单,不过现在要让其更加容易理解一些。
首先发现底价(basePrice)等于数量(quantity)乘以单价(item price)。于是可以把这一部分的计算结果放进一个临时变量中,同时将Math.min()函数中参数进行同样替换。

  1. double price(){
  2. // 价格 = basePrice - quantity discount + shipping
  3. final double basePrice = _quantity * _itemPrice;
  4. return basePrice -
  5. Math.max(0, _quantity - 800) * _itemPrice * 0.15 +
  6. Math.min(basePrice * 0.25, 100);
  7. }

然后,将批发折扣(quantity discount)的计算提炼出来,并将运算结果赋予临时变量。

  1. double price(){
  2. // 价格 = basePrice - quantity discount + shipping
  3. final double basePrice = _quantity * _itemPrice;
  4. final double quantityDiscount = Math.max(0, _quantity - 800) * _itemPrice * 0.15;
  5. return basePrice -quantityDiscount+
  6. Math.min(basePrice * 0.25, 100);
  7. }

最后,再把搬运费(shipping)计算提炼出来,并将运算结果赋予临时变量。

  1. //重构后
  2. double price(){
  3. // 价格 = basePrice - quantity discount + shipping
  4. final double basePrice = _quantity * _itemPrice;
  5. final double quantityDiscount = Math.max(0, _quantity - 800) * _itemPrice * 0.15;
  6. final double shipping = Math.min(basePrice * 0.25, 100);
  7. return basePrice - quantityDiscount + shipping;
  8. }

运用提炼函数处理
对于上述代码,通常不以临时变量来解释其动作意图,而是更喜欢使用提炼函数。

  1. //重构前
  2. double price(){
  3. // 价格 = basePrice - quantity discount + shipping
  4. return _quantity * _itemPrice -
  5. Math.max(0, _quantity - 800) * _itemPrice * 0.15 +
  6. Math.min(_quantity * _itemPrice * 0.25, 100);
  7. }

现在把底价计算提炼到一个独立的函数中。

  1. double price(){
  2. // 价格 = basePrice - quantity discount + shipping
  3. return basePrice() -
  4. Math.max(0, _quantity - 800) * _itemPrice * 0.15 +
  5. Math.min(basePrice() * 0.25, 100);
  6. }
  7. private double basePrice(){
  8. return _quantity * _itemPrice;
  9. }

继续进行提炼,每次提炼一个新的函数。最后得到代码如下。

  1. //重构后
  2. double price(){
  3. // 价格 = basePrice - quantity discount + shipping
  4. return basePrice() - quantityDiscount() + shipping();
  5. }
  6. private double basePrice(){
  7. return _quantity * _itemPrice;
  8. }
  9. private double shipping(){
  10. return Math.min(basePrice() * 0.25, 100);
  11. }
  12. private double quantityDiscount(){
  13. return Math.max(0, _quantity - 800) * _itemPrice * 0.15;
  14. }

分解临时变量

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

  1. //重构前
  2. double temp = 2 * (_height + _width);
  3. System.out.println(temp);
  4. temp = _height + _width;
  5. System.out.println(temp);
  1. //重构后
  2. final double perimeter = 2 * (_height + _width);
  3. System.out.println(perimeter);
  4. final double area = _height + _width;
  5. System.out.println(area);

动机

在某些情况下,临时变量用于保存一段冗长代码的运算结果,以便稍后使用。
这种临时变量应该只被赋值一次。
如果它被赋值超过一次,就意味着它们在函数中承担了一个以上的责任。
如果临时变量承担多个责任,它就应该被替换(分解)为多个临时变量,使得每一个变量只承担一个责任
同一个临时变量承担两件不同的事情,会让代码阅读者糊涂。

做法

(1)在待分解临时变量的声明及第一次被赋值处,修改其名称。
(2)将新的临时变量声明为final。
(3)以该临时变量的第二次赋值动作为界,修改此前对该临时变量的所有引用点,让它们引用新的临时变量。
(4)在第二次赋值处,重新声明原先那个临时变量。
(5)编译,测试。
(6)逐次重复上述过程。每次都在声明处对临时变量改名,并修改下次赋值之前的引用点。

示例

我们从一个简单计算开始:我们需要计算一个苏格兰布丁运动的距离。在起点处,静止的布丁会受到一个初始力的作用而开始运动。一段时间后,第二个力作用于布丁,让它再次加速。根据牛顿第二定律,计算布丁运动距离:

牛顿第二定律
内容:物体的加速度与所受合外力成正比,跟物体的质量成反比。
表达式:F=ma。
物理意义:反映物体运动的加速度大小、方向与所受合外力的关系,且这种关系是瞬时的。

  1. double getDistance(int time){
  2. double result;
  3. double acc = _primaryForce / _mass;
  4. int primaryTime = Math.min(time, _delay);
  5. result= 0.5 * acc * primaryTime * primaryTime;
  6. int secondaryTime = time - _delay;
  7. if(secondaryTime > 0){
  8. double primaryVel = acc *_delay;
  9. acc = (_primaryForce + _secondaryForce) / _mass;
  10. result += primaryVel * secondaryTime + 0.5 * acc * secondaryTime * secondaryTime;
  11. }
  12. return result;
  13. }

代码看起来好像有点丑陋。观察例子中的acc变量是如何被赋值两次。
acc变量有两个责任,一是保存第一个力产生的加速度;二是保存两个力共同产生的加速度。这就是需要分解的东西。
首先,在函数开始修改处修改这个临时变量的名称,并将新的临时变量声明为final。然后,把第二次赋值之前对acc变量的所有引用点,全部改用心的临时变量。最后,在第二次赋值处重新声明acc变量。

  1. double getDistance(int time){
  2. double result;
  3. final double primaryAcc = _primaryForce / _mass;
  4. int primaryTime = Math.min(time, _delay);
  5. result= 0.5 * primaryAcc * primaryTime * primaryTime;
  6. int secondaryTime = time - _delay;
  7. if(secondaryTime > 0){
  8. double primaryVel = primaryAcc *_delay;
  9. double acc = (_primaryForce + _secondaryForce) / _mass;
  10. result += primaryVel * secondaryTime + 0.5 * acc * secondaryTime * secondaryTime;
  11. }
  12. return result;
  13. }

新的临时变量指出,它只承担原先acc变量的第一个责任。
将它声明为final,确保它只被赋值一次
然后,在原先acc变量第二次被赋值处重新声明acc。
现在,重新编译并测试,一切都没有问题。
然后继续处理acc临时变量的第二次赋值

  1. double getDistance(int time){
  2. double result;
  3. final double primaryAcc = _primaryForce / _mass;
  4. int primaryTime = Math.min(time, _delay);
  5. result= 0.5 * primaryAcc * primaryTime * primaryTime;
  6. int secondaryTime = time - _delay;
  7. if(secondaryTime > 0){
  8. double primaryVel = primaryAcc *_delay;
  9. final double secondaryAcc = (_primaryForce + _secondaryForce) / _mass;
  10. result += primaryVel * secondaryTime + 0.5 * secondaryAcc * secondaryTime * secondaryTime;
  11. }
  12. return result;
  13. }

以查询取代临时变量 手法进行重构

  1. //“以查询取代临时变量”手法进行重构
  2. double getDistance(int time){
  3. double result= 0.5 * getPrimaryAcc() * getPrimaryTime(time) * getPrimaryTime(time);
  4. if(getSecondaryTime(time) > 0){
  5. result += getSeconddistance();
  6. }
  7. return result;
  8. }
  9. private double getPrimaryAcc(){
  10. return _primaryForce / _mass;
  11. }
  12. private double getSecondaryAcc(){
  13. return (_primaryForce + _secondaryForce) / _mass;
  14. }
  15. private int getPrimaryTime(int time){
  16. return Math.min(time, _delay);
  17. }
  18. private int getSecondaryTime(int time){
  19. return time - _delay;
  20. }
  21. private double getSeconddistance(){
  22. return getPrimaryAcc() *_delay * getSecondaryTime(time)
  23. + 0.5 * getSecondaryAcc() * getSecondaryTime(time) * getSecondaryTime(time);
  24. }

移除对参数的赋值

问题:代码对一个参数进行赋值
解决:以一个临时变量取代该参数的位置。

  1. //重构前
  2. int dicount(int inputVal, int quantity, int yearToDate){
  3. if(inputVal > 50) inputVal-=10;
  4. }
  1. //重构后
  2. int dicount(final int inputVal, int quantity, int yearToDate){
  3. int result = inputVal;
  4. if(result > 50) result-=10;
  5. }

动机

我想你很清楚“对参数赋值”这个说话的意思。
如果把一个名称为fool的对象作为参数传递给某个函数,那么“对参数赋值”意味改变fool,使它引用另一个对象
但是,如果在“被传入对象”身上进行什么操作,那没问题,我们经常会这么做。
这里只针对“fool被改变而指向另一个对象”这种情况来讨论:

  1. void test(Object fool){
  2. fool.changedBySomeWay(); //that's ok
  3. fool=anotherObject; //trouble will appear
  4. }

我们之所不这样做,是因为它降低了代码的清晰度,而且混用了按值传递和按引用传递这两种参数传递方式。
JAVA只采用按值进行传递
在按值传递的情况下,对参数的任何修改,都不会对调用端造成任何影响
如果你只以参数表示“被传递进来的东西”,那么代码会清晰的多,因为这种用法在所有语言中都表现出相同的语义。
在JAVA中,一般不要对参数赋值:如果你看到手上的代码已经这么做了,就应该使用本文的方法。

做法

(1)建立一个临时变量,把待处理的参数值赋赋予它。
(2)以“对参数的赋值”为界,将其后所有对此参数的引用点,全部替换为“对此临时变量的引用”。
(3)修改赋值语句,使其改为对新建之临时变量赋值。
(4)编译,测试。
(如果代码的语义是按照引用传递的,需在调用端检查调用后是否还使用了这个参数。也要检查有多少个按引用传递的参数被赋值后又被使用。应该尽量以return方式返回一个值。如果返回值有多个,可考虑将需返回的一大堆数据变为对象,或者为每个返回值设定一个独立的函数)

示例

  1. int dicount(int inputVal, int quantity, int yearToDate){
  2. if(inputVal > 50) inputVal-=5;
  3. if(quantity > 100) quantity-=10;
  4. if(yearToDate > 1000) yearToDate-=100;
  5. return inputVal;
  6. }

以临时变量取代对参数的赋值动作,得到下列代码:

  1. int dicount(int inputVal, int quantity, int yearToDate){
  2. int result = inputVal;
  3. if(result > 50) result-=5;
  4. if(quantity > 100) quantity-=10;
  5. if(yearToDate > 1000) yearToDate-=100;
  6. return result;
  7. }

可以为参数加上final关键词,强制其遵循“不对参数赋值”这一惯例:

  1. int dicount(final int inputVal, final int quantity, final int yearToDate){
  2. int result = inputVal;
  3. if(result > 50) result-=5;
  4. if(quantity > 100) quantity-=10;
  5. if(yearToDate > 1000) yearToDate-=100;
  6. return result;
  7. }

JAVA的按值传递

我们应该都知道,JAVA使用按值传递的函数调用方式,这常常也会使大家迷惑。在所有地点,JAVA都会遵循严格按值传递:

  1. //JAVA按值的传递
  2. class Params{
  3. public static void main(String[] args) {
  4. int x = 10;
  5. triple(x);
  6. System.err.println("x after triple:" + x);
  7. }
  8. private static void triple(int arg){
  9. arg = arg * 3;
  10. System.err.println("arg in triple:" +arg );
  11. }
  12. }
  13. //输出
  14. //arg in triple:30
  15. //x after triple:10

上面代码是使用基本数据类型进参数传递,还不至于让人糊涂。但如果参数中传递的是对象,就可能把人弄糊涂。如果在程序中以Date对象表示日期,下列程序所示:

  1. //以对象为参数
  2. class Params{
  3. public static void main(String[] args) {
  4. Date d1 = new Date(2015,1,1);
  5. nextDateUpdate(d1);
  6. System.err.println("d1 after nextday:" + d1);
  7. Date d2 = new Date(2015,1,1);
  8. nextDateReplace(d2);
  9. System.err.println("d2 after nextday:" + d2);//61380864000000
  10. }
  11. private static void nextDateUpdate(Date d) {
  12. d.setDate(d.getDate()+1);
  13. System.err.println("arg in nextday d1 : "+d);
  14. }
  15. private static void nextDateReplace(Date d) {
  16. d = new Date(d.getYear(),d.getMonth(),d.getDate()+1);
  17. d=null;
  18. System.err.println("arg in nextday d2: "+d);
  19. }
  20. }
  21. //输出
  22. /*
  23. arg in nextday d1 : Tue Feb 02 00:00:00 CST 3915
  24. d1 after nextday: Tue Feb 02 00:00:00 CST 3915
  25. arg in nextday d2: Tue Feb 02 00:00:00 CST 3915
  26. d2 after nextday: Mon Feb 01 00:00:00 CST 3915
  27. */

从本质上说,对象的引用是按值传递的。因为可以修改参数对象的内部状态,但对参数对象重新赋值是没有意义的。

以函数对象取代函数

问题:你有一个大型函数,其中对局部变量的使用使你无法采用“提炼函数”这种重构手法
解决:将这个函数放进一个单独对象中,这样,局部变量就成了对象的字段,然后就可以在同一个对象中将这个大型函数分解为多个小型函数。

  1. //重构前
  2. class Order....
  3. double price(){
  4. double basePrice;
  5. double secondaryPrice;
  6. double thirdaryPrice;
  7. //compute()
  8. ......
  9. }
  1. //重构后
  2. class Order...
  3. double price(){
  4. return new PriceCalculator(this).compute();
  5. }
  6. class PriceCalculator{
  7. double basePrice;
  8. double secondaryPrice;
  9. double thirdaryPrice;
  10. double compute(){
  11. //...
  12. }
  13. }

动机

在前面的文章中一直在强调小型函数的优美动人。
只要将相对独立的代码从大函数提炼出来,就可以大大提高代码的可读性。
但是局部变量的存在会增加函数分解的难度
如果一个函数中的局部变量泛滥成灾,那么想分解这个函数是非常困难的。
“以查询替换临时变量”手法可以帮助减轻负担,但有时候还是会发现根本无法拆解一个需要拆解的函数。
这种情况就应该考虑使用函数对象来解决。本文的重构方法会将所有的局部变量都变成函数对象的字段。然后就可以使用“提炼函数”创造新的函数,从而将原来的大型函数拆解变小。

做法

(1)建立一个新的类,根据待处理函数的用途为其命名。
(2)在新类中建立一个final字段,用以保存原先大型函数所在对象。针对原函数的每个临时变量和每个参数,在新类中建立一个对应的字段保留之。
(3)在新类中建立一个构造函数,接收原对象以原函数的所有参数作为其参数。
(4)在新类中建立一个compute()函数。
(5)将原函数的代码复制到compute()函数中。如果需要调用源对象的任何函数,通过原对象字段调用。
(6)编译,测试。
由于所有局部变量现在都成了字段,所以你可以任意分解这个大型函数,不必传递任何参数。

示例

  1. class Account{
  2. int gamm(int value, int quantity, int year2Date){
  3. int importValue1 = (value * quantity) + delta();
  4. int importValue2 = (value * year2Date) + 200;
  5. if(year2Date - importValue1 >200)
  6. importValue2-=50;
  7. int importValue3 = importValue2 * 8;
  8. //......
  9. return importValue3 - 2 * importValue1;
  10. }
  11. //.....
  12. }

为了把这个函数变为函数对象,首先需要声明一个新类。在新类中提供final对象保存原对象,对于函数的每个参数和每个临时变量,也以一个字段逐一保留。

  1. class Gamm{
  2. private final Account _account;
  3. private int value;
  4. private int quantity;
  5. private int year2Date;
  6. private int importValue1;
  7. private int importValue2;
  8. private int importValue3;

接下来,加入一个构造函数。

  1. Gamm(Account source, int inputVal, int quantity, int year2Date){
  2. this._account = source;
  3. this.value = inputVal;
  4. this.quantity = quantity;
  5. this.year2Date = year2Date;
  6. }

现在可以把原本函数搬到compute()中了。函数中任何调用Accout类的地方,都必改用_account字段。

  1. int compute(){
  2. importValue1 = (value * quantity) + _account.delta();
  3. importValue2 = (value * year2Date) + 200;
  4. if(year2Date - importValue1 >200)
  5. importValue2-=50;
  6. importValue3 = importValue2 * 8;
  7. //......
  8. return importValue3 - 2 * importValue1;
  9. }

然后,修改旧函数,让它将工作委托给刚完成的这个函数对象。

  1. int gamm(int value, int quantity, int year2Date){
  2. return new Gamm(this,value,quantity,year2Date).compute();
  3. }

以上就是本文重构方法的基本原则。其所带来的好处是:现在可以轻松地对compute()函数采取“提炼函数”,而不必担心参数传递的问题。

  1. //运用提炼函数 不必担心参数问题
  2. int compute(){
  3. importValue1 = (value * quantity) + _account.delta();
  4. importValue2 = (value * year2Date) + 200;
  5. importantThing();
  6. importValue3 = importValue2 * 8;
  7. //......
  8. return importValue3 - 2 * importValue1;
  9. }
  10. private void importantThing() {
  11. if(year2Date - importValue1 >200)
  12. importValue2-=50;
  13. }

主要介绍了重构手法——以函数对象取代函数。
我们都不喜欢临时变量巨多的方法,那只会让我们迷惑。
对于局部变量很多的函数,有必要运用本文的重构方法进行处理,将其转化为函数对象,那样就把临时变量转为函数对象的字段,继而可以进行其它重构方法。