从一个简单Java程序来谈谈重构

时间:2023-01-28 20:50:58
这个主题是关于编码的一些原则和模式的,用这些可以帮助程序员创建更加灵活和具有适应性的软件模块。

       笔者下面引用的程序是Robert大叔著名的程序片断来重新认识一下重构,那些java代码看起来正确,但事实上不是看起来那么简单的,一小段程序调试起来总会有些小错误。我一度怀疑是作者或译者故意去写错一些代码,然后引起阅读者的注意的。从程序的调试编写以及重构过程中,备注了作为一名一线程序员的一些总结。

     “重构”,顾名思义,对程序来说就是“在不改变代码外在行为的前提下对代码做出修改,以改进代码内部结构的过程”。

 

       下面是一个素数产生程序,首先要知道什么是素数吧?记得上学时书上说素数就是质数,是除了能被1和本身整除外,没有其他因子能整除。又google了下,其定义如下:

        1.只有1和它本身这两个因数的自然数叫做素数。还可以说成素数只有1和它本身两个约数。

        2.素数是这样的整数,它除了能表示为它自己和1的乘积以外,不能表示为任何其它两个整数的乘积。

        例如,15=3×5,所以15不是素数.

 

       从编程角度我们要考虑其算法,算法是什么?算法采用Sieve of Eratosthenes筛选法,这个算法的详细情况是这样的:

       由于一个合数(相对于素数的定义,即0和1之外,除了素数就是合数)总是可以分解成若干个素数的乘积,那么如果把素数(最初只知道2为素数)的倍数都去掉,那么剩下的就是素数了。

       例如要查找100以内的素数,首先2是质数,把2的倍数去掉;此时3没有被去掉,可认为是素数,所以把3的倍数去掉;再到5,再到7,之后呢因为8,9,10刚都被去掉了,而100以内的任意合数肯定都有一个因子小于10(100的开方)。因此去掉2,3,5,7的倍数后剩下的都是素数了。

 

       下面程序的主要逻辑片断是:

       

[javascript] view plain copy
  1. //最主要逻辑在于此,Math.sqrt(s)取得该数位于中间的因子.  
  2.           //比如maxValue=10,那么Math.sqrt(s)=3          
  3.           for(i=2; i< Math.sqrt(s)+1;i++){  
  4.               //下面算法举例子,比如maxValue=10时  
  5.               //循环1:i=2  
  6.               //循环2:i=3   
  7.               //循环3:i=4  
  8.               for(j=2*i;j<s;j+=i){  
  9.                   //循环11:j=4;j<11;j=4+2  
  10.                   //循环12:j=6;j<11;j=6+2  
  11.                   //循环13:j=8;j<11;j=8+2  
  12.                   //循环14:j=10;j<11;j=10+2  
  13.                   //这样就过滤掉4,6,9,10  
  14.                   
  15.                   //循环21:j=6;j<11;j=6+3  
  16.                   //循环22:j=9;j<11;j=9+3    
  17.                   //这样就过滤掉9  
  18.                     
  19.                   //循环31:j=8;j<11;j=8+4                  
  20.                   f[j] = false;  
  21.               }  
  22.           }  

      

        素数产生程序:一个简单的重构示例

        version1:GeneratePrimesV1.java

        下面的为实现需求“产生给定整数范围内所有的素数”写的第一个版本Verson1,这个版本的程序把实现需求的3个小功能全部糅合在了一个类文件中,耦合性太强。具体代码如下: 

       

  1. package Prime;  
  2.   
  3. public class GeneratePrimesV1 {  
  4.   
  5.     /** 
  6.      * The algorithm is quite simple. Given an array of integers 
  7.      * starting at 2.Cross out all multiples of 2.Find the next  
  8.      * uncrossed integer,and cross out all of its multiples.Repeat 
  9.      * until you have passed the squre root of the maximum value. 
  10.      *@author ZHANGRONG 
  11.      *@version 1.0 
  12.      *@param int maxValue 
  13.      *@return int[] 
  14.      *质数又称素数。 
  15.      *指在一个大于1的自然数中,除了1和此整数自身外,没法被其他自然数整除的数。 
  16.      *换句话说,只有两个正因数(1和自己)的自然数即为素数。 
  17.      *比1大但不是素数的数称为合数。 
  18.      *1和0既非素数也非合数. 
  19.      */  
  20.     public static int[] generatePrimes(int maxValue){  
  21.           
  22.         if(maxValue >= 2){  
  23.             
  24.           int s = maxValue + 1;  
  25.           boolean[] f = new boolean[s];  
  26.           int i;  
  27.             
  28.           for(i=0;i<s;i++){  
  29.               f[i] = true;  
  30.           }  
  31.             
  32.           f[0] = f[1] = false;  
  33.             
  34.           int j;  
  35.           //最主要逻辑在于此,Math.sqrt(s)取得该数位于中间的因子.  
  36.           //比如maxValue=10,那么Math.sqrt(s)=3          
  37.           for(i=2; i< Math.sqrt(s)+1;i++){  
  38.               //下面算法举例子,比如maxValue=10时  
  39.               //循环1:i=2  
  40.               //循环2:i=3   
  41.               //循环3:i=4  
  42.               for(j=2*i;j<s;j+=i){  
  43.                   //循环11:j=4;j<11;j=4+2  
  44.                   //循环12:j=6;j<11;j=6+2  
  45.                   //循环13:j=8;j<11;j=8+2  
  46.                   //循环14:j=10;j<11;j=10+2  
  47.                   //这样就过滤掉4,6,9,10  
  48.                   
  49.                   //循环21:j=6;j<11;j=6+3  
  50.                   //循环22:j=9;j<11;j=9+3    
  51.                   //这样就过滤掉9  
  52.                     
  53.                   //循环31:j=8;j<11;j=8+4                  
  54.                   f[j] = false;  
  55.               }  
  56.           }  
  57.             
  58.           int count =0;  
  59.           for(i=0;i<s;i++){  
  60.               if(f[i]){  
  61.                   count++;  
  62.               }  
  63.           }  
  64.             
  65.           int[] primes = new int[count];            
  66.           for(i=0,j=0;i<s;i++){  
  67.               if(f[i]){  
  68.                   primes[j++] = i;  
  69.               }  
  70.           }  
  71.             
  72.           return primes;  
  73.         }else{  
  74.             return new int[0];  
  75.         }         
  76.     }  
  77.       
  78.     public static void main(String args[]){  
  79.         int maxValue = 10;  
  80.           
  81.         System.out.println(generatePrimes(maxValue));  
  82.     }  
  83. }  

        

        version2:GeneratorPrimeV2.java

         上述程序为实现功能,可以分成3个步骤,这3个步骤是:

        1.第一对所有的变量进行初始化,并做好过滤所需要的准备工作;

        2.第二执行真正的过滤工作;

        3.第三把过滤后的结果存放到一个整型数组中。

        在将上述3个步骤写成3个小功能时,把一些函数级的局部变量提升为类级的静态域。

        实现程序如下:

        

  1. package Prime;  
  2.   
  3. public class GeneratorPrimeV2 {  
  4.   
  5.     private static boolean[] f;  
  6.     private static int[] primes;  
  7.       
  8.     public static int[] generatePrimes(int maxValue){  
  9.         if(maxValue < 2){  
  10.             return new int[0];  
  11.         }else{  
  12.             int s= maxValue + 1;  
  13.             initializeSieve(s);  
  14.             sieve(s);  
  15.             loadPrimes(s);  
  16.             return primes;  
  17.         }         
  18.     }  
  19.       
  20.     private static void initializeSieve(int s){  
  21.         f= new boolean[s];  
  22.         int i;  
  23.           
  24.         for(i=0;i<s;i++){  
  25.             f[i] = true;  
  26.         }  
  27.         f[0]=f[1]=false;  
  28.     }  
  29.       
  30.     private static void sieve(int s){  
  31.         int i;  
  32.         int j;  
  33.           
  34.         for(i=2;i<Math.sqrt(s)+1;i++){  
  35.             if(f[i]){  
  36.                 for(j=2*i;j<s;j+=i)  
  37.                 f[j] = false;                     
  38.             }  
  39.         }  
  40.     }     
  41.       
  42.     private static void loadPrimes(int s){  
  43.         int i ;  
  44.         int j;  
  45.         int count = 0;  
  46.           
  47.         for(i=0;i<s;i++){  
  48.             if(f[i]){  
  49.                 count++;  
  50.             }  
  51.         }  
  52.           
  53.         primes = new int[count];  
  54.           
  55.         for(i=0,j=0;i<s;i++){  
  56.             if(f[i]){  
  57.                 primes[j++] = i;  
  58.             }  
  59.         }  
  60.     }  
  61.               
  62.     public static void main(String args[]){  
  63.         int maxValue = 10;  
  64.           
  65.         System.out.println(generatePrimes(maxValue));  
  66.     }  
  67. }  

         version3:GeneratePrimesV3。java

         最后,还对变量名和方法名称做更正,使其更符合实际上实现的意义。有人也许觉得更改名字的工作比较琐碎,但是对于代码的可读性以及维护成本是大有好处的。还有关键逻辑部分有关于数组长度的平方根问题,那个平方根未必就是素数,那个方法没有计算出罪的素数因子,说明性的注释是有误的。所以对注释重新写了,使他可以更好表达代码的平方根后面的原理,并且适当的改了变量的名字。

        至于+1在那里起了什么作用呢?担心具有小数位的平方根会转换为小一点的整数,以至于不能充当遍历的上限。但是这种做法有必要吗?真正的遍历上限是小于或者等于数组长度平方根的最大素数。于是应该去掉+1,毫不犹豫。

        

  1. package Prime;  
  2.   
  3. public class GeneratePrimesV3 {  
  4.   
  5.     private static boolean[] crossedOut;  
  6.     private static int[] result;  
  7.       
  8.     public static int[] generatePrimes(int maxValue){  
  9.         if(maxValue < 2){  
  10.             return new int[0];  
  11.         }else{  
  12.             uncrossIntegersUpTo(maxValue);  
  13.             crossOutMultiples();  
  14.             putUncrossedIntegersIntoResult();  
  15.             return result;  
  16.         }  
  17.     }  
  18.       
  19.     private static void uncrossIntegersUpTo(int maxValue){  
  20.         crossedOut = new boolean[maxValue + 1];  
  21.         for(int i=2;i<crossedOut.length;i++){  
  22.             crossedOut[i] = false;  
  23.         }  
  24.     }  
  25.       
  26.     private static void crossOutMultiples(){  
  27.         int limit = determineIterationLimit();  
  28.         for(int i=2;i<limit;i++){  
  29.             if(notCrossed(i)){  
  30.                 crossOutMultiplesOf(i);  
  31.             }  
  32.         }  
  33.     }  
  34.       
  35.     private static int determineIterationLimit(){  
  36.         double iterationLimit = Math.sqrt(crossedOut.length);  
  37.         return (int)iterationLimit;  
  38.     }  
  39.       
  40.     private static void crossOutMultiplesOf(int i){  
  41.         for(int multiple=2*i;multiple<crossedOut.length;multiple+=i){  
  42.             crossedOut[multiple]=true;  
  43.         }  
  44.     }  
  45.       
  46.     private static boolean notCrossed(int i){  
  47.         return crossedOut[i]==false;  
  48.     }  
  49.       
  50.     private static void putUncrossedIntegersIntoResult(){  
  51.         result = new int[numberOfUncrossedIntegers()];  
  52.         for(int j=0,i=2;i<crossedOut.length;i++){  
  53.             if(notCrossed(i)){  
  54.                 result[j++]=i;  
  55.             }  
  56.         }  
  57.     }  
  58.       
  59.     private static int numberOfUncrossedIntegers(){  
  60.         int count = 0;  
  61.         for(int i=2;i<crossedOut.length;i++){  
  62.             if(notCrossed(i)){  
  63.                 count++;  
  64.             }  
  65.         }  
  66.         return count;  
  67.     }  
  68. }  

         测试程序,TestGeneratePrimes.java

         下面的测试程序有个大的特点,与原来用junit.framework.*下的TestCase不同,在mian方法中添加一个框架的main方法启动图形用户界面(GUI)来进行测试,是用swing做的,之前我用过swing写过一点简单的socket的即时通的小程序,好久没有用了。下面这样使用,运行出界面后,很是惊喜,运行结果用图片抓了出来,本来想放到这边博文中的,可是总是找不到csdn插入图片的工具,找到了后,也插入不进去,很是郁闷。

        下面测试为了防止在改变程序后,有的边角没有测试到,另外一个方法 testExhaustive()来检查2~500之间产生的素数列表中没有倍数存在。

       

  1. package Prime;  
  2.   
  3. import junit.framework.*;  
  4. /** 
  5.  * @version 1.3 
  6.  * @author ZHANGRONG 
  7.  * 
  8.  */  
  9. public class TestGeneratePrimes  extends TestCase{  
  10.   
  11.     /** 
  12.      * @param args 
  13.      */  
  14.     public static void main(String[] args) {  
  15.         // TODO Auto-generated method stub  
  16.      String[] testCaseName = { TestGeneratePrimes.class.getName() };  
  17.   
  18.      junit.swingui.TestRunner.main(testCaseName);  
  19.     }  
  20.       
  21.     public TestGeneratePrimes(String name){  
  22.         super(name);  
  23.     }  
  24.       
  25.     public void testPrimes(){  
  26.         int[] nullArray = GeneratePrimesV3.generatePrimes(0);  
  27.         assertEquals(nullArray.length,0);  
  28.           
  29.         int[] minArray = GeneratePrimesV3.generatePrimes(2);  
  30.         assertEquals(minArray.length,1);  
  31.         assertEquals(minArray[0],2);  
  32.           
  33.         int[] threeArray = GeneratePrimesV3.generatePrimes(3);  
  34.         assertEquals(threeArray.length,2);  
  35.         assertEquals(threeArray[0],2);  
  36.         assertEquals(threeArray[1],3);  
  37.           
  38.         int[] centArray = GeneratePrimesV3.generatePrimes(100);  
  39.         assertEquals(centArray.length,25);  
  40.         assertEquals(centArray[24],97);  
  41.     }  
  42.       
  43.     public void testExhaustive(){  
  44.         for(int i=2;i<500;i++){  
  45.             verifyPrimeList(GeneratePrimesV3.generatePrimes(i));  
  46.         }  
  47.     }  
  48.       
  49.     private void verifyPrimeList(int[] list){  
  50.         for(int i=0;i<list.length;i++){  
  51.             verifyPrime(list[i]);  
  52.         }  
  53.     }  
  54.       
  55.     private void verifyPrime(int n){  
  56.         for(int factor=2;factor<n;factor++){  
  57.             System.out.print((n%factor!=0));  
  58.         }  
  59.     }  
  60.   
  61. }  

       

        

          

    结论:

         写完这3个版本并且逐一测试后,发现重构后的程序读起来比一开始要好得多。程序变得更易理解,因此也更容易修改、变更,程序结构的各部分之间相互隔离,耦合性比较小。



from:http://blog.csdn.net/zhang13579rong/article/details/5307346