重构初体验

设计大师Martin Fowler 在《重构——改善既有代码的设计》一书中,以其精妙的概括能力,彻底对重构技术作了全方位的总结。该书既具备大百科全书般提纲挈领的重构大纲,同时更通过实例展现了在软件设计中重构的魅力。

有感于重构艺术予我的震撼,我逐渐尝试在项目设计中开始重构之旅。在这个旅程中,存在尝试的犹豫和领悟的感动,然而最终却令我折服。如今,我希望能通过一个实际的例子,让读者也能初次体验重构的魅力。举例来说,我打算作一个容器,用来存放每个整数的阶乘
结果。最初的设计是这样:
public class FactorialContainer
{
       public FactorialContainer()
       {
              factorialList = new ArrayList();
       }

       public FactorialContainer(int capacity)
       {
              capa = capacity;
              factorialList = new ArrayList(capacity);
       }

       private ArrayList factorialList;
       private int capa;

       public ArrayList FacotorialList
       {
              get {return factorialList;}
              set {factorialList = value;}
       }
       public int Capacity
       {
              get {return capa;}
              set {capa = value;}
       }
       public long this[int index]
       {
              get {return (long)factorialList[index];}
              set {factorialList[index] = value;}
       }
       public void Add()
       {
              long result = 1;
              int seed = factorialList.Count + 1;
              for (int i=1;i< =seed;i++)
              {
                     result*=i;
              }
              factorialList.Add(result);
       }         
       public void Clear()
       {
              factorialList.Clear();
       }
       public void RemoveAt(int index)
       {
              factorialList.RemoveAt(index);
       }
}

熟悉重构的人是否已经嗅到了代码的坏味道了呢?是的,在Add()方法里,将计算阶乘的算法也放到了里面。由于这些代码实现了独立的算法,因此应该利用Extract Method规则,将这些代码提炼出来,形成独立的方法:
public void Add()
{
       long result = CountFactorial();           
       factorialList.Add(result);
}           
private long CountFactorial()
{
       long result = 1;
       int seed = factorialList.Count + 1;
       for (int i=1;i<=seed;i++)
       {
              result*=i;
       }
       return result;
}

我们还可以进一步简化Add()方法:
public void Add()
{
       factorialList.Add(Count());
}

现在我希望扩充这个容器的功能,加入菲波那契数列的计算。由于两者的计算方式是完全不同的,因此需要重新创建一个菲波那契容器:
public class FibonacciContainer
{
       public FibonacciContainer()
       {
              fibonacciList = new ArrayList();
       }
       public FibonacciContainer(int capacity)
       {
              capa = capacity;   
              fibonacciList = new ArrayList();
       }

       private ArrayList fibonacciList;
       private int capa;

       public ArrayList FibonacciList
       {
              get {return fibonacciList;}
              set {fibonacciList = value;}
       }
       public int Capacity
       {
              get {return capa;}
              set {capa = value;}
       }
       public long this[int index]
       {
              get {return (long)fibonacciList[index];}
              set {fibonacciList[index] = value;}
       }
       public void Add()
       {
              fibonacciList.Add(CountFibonacci ());
       }
       public void RemoveAt(int index)
       {
              fibonacciList.RemoveAt(index);
       }
       public void Clear()
       {
              fibonacciList.Clear();
       }
       private long CountFibonacci ()
       {
              long result = 0;
              int seed = fibonacciList.Count;
              if (seed == 0 || seed == 1)
              {
                     result = 1;
              }
              else
              {
                     result = this[seed-1] + this[seed-2];
              }
              return result;
       }
}

比较上面两段容器的代码,会有很多相似之处。又是时候拿起重构的利器了。首先我们根据name Method规则,将计算阶乘和菲波那契数列的方法改名为统一的名字。为什么要改名呢?既然两个容器有着相似之处,为什么不能定义一个基类,然后从其派生出各自的类呢?为了保证类方法的一致性,当然有必要对方法重新命名了。实际上,我们不仅要重命名方法名,而且还要改变属性的名字。

FacotorialList 、FibonacciList:改名为MathList;
CountFactorial()、CountFibonacci():改名为Count();

然后再通过Extract Class和Extract SubClass规则抽象出基类MathClass。

最后的代码为:
基类:MathContainer
public abstract class MathContainer
{
       public MathContainer()
       {
              mathList = new ArrayList();
       }      
       public MathContainer(int capacity)
       {
              capa = capacity;
              mathList = new ArrayList(capacity);
       }
       private ArrayList mathList;
       private int capa;
       public ArrayList MathList
       {
              get {return mathList;}
              set {mathList = value;}
       }
       public int Capacity
       {
              get {return capa;}
              set {capa = value;}
       }
       public long this[int index]
       {
              get {return (long)mathList[index];}
              set {mathList[index] = value;}
       }
       public void Add()
       {
              mathList.Add(Count());
       }
       public void RemoveAt(int index)
       {
              mathList.RemoveAt(index);
       }
       public void Clear()
       {
              mathList.Clear();
       }
       protected abstract long Count();     
}

然后从基类分别派生出计算阶乘和菲波那契数列的容器类:
派生类:FactorialContainer
public class FactorialContainer:MathContainer
{
       public FactorialContainer():base(){}
       public FactorialContainer(int capacity):base(capacity){}   
       protected override long Count()
       {
              long result = 1;
              int seed = MathList.Count + 1;
              for (int i=1;i<=seed;i++)
              {
                     result*=i;
              }
              return result;
       }
}

派生类:FibonacciContainer
public class FibonacciContainer:MathContainer
{
       public FibonacciContainer():base(){}
       public FibonacciContainer(int capacity):base(capacity){}
       protected override long Count()
       {
              long result = 0;
              int seed = MathList.Count;
              if (seed == 0 || seed == 1)
              {
                     result = 1;
              }
              else
              {
                     result = this[seed-1] + this[seed-2];
              }
              return result;
       }
}

UML类图如下:

对于这样的程序结构,要扩展起来是很容易的,例如素数的容器,我们只需要定义PrimeNumberContainer类,然后重写Count()方法,并派生MathContainer类即可。
 
经过重构,程序的结构变得愈发清晰。如果我们再仔细分析现在的结构,对于算法的扩展是非常容易的,但如何创建每个容器的实例,似乎还有更好的方法,那就是通过工厂来管理每个实例的创建。因为产品只有一类,所以可以参用工厂方法模式(Factory Method)。首先我们来看看UML类图:

实现代码如下:
基类工厂:MathFacotry类
public abstract class MathFactory
{
       public abstract MathContainer CreateInstance();
       public abstract MathContainer CreateInstance(int capacity);
}

阶乘容器工厂:FactorialFactory
public class FactorialFactory:MathFactory
{
       public override MathContainer CreateInstance()
       {
              return new FactorialContainer();
       }
       public override MathContainer CreateInstance(int capacity)
       {
              return new FactorialContainer(capacity);
       }
}

菲波那契数列容器工厂:
public class FibonacciFactory:MathFactory
{
       public override MathContainer CreateInstance()
       {
              return new FibonacciContainer();
       }
       public override MathContainer CreateInstance(int capacity)
       {
              return new FibonacciContainer(capacity);
       }
}

有了工厂,就可以通过工厂来创建我们所需要的具体容器类对象了:
[STAThread]
static void Main(string[] args)
{
       MathFactory factory1 = new FactorialFactory();
       MathFactory factory2 = new FibonacciFactory();

       MathContainer factorial = factory1.CreateInstance();
       MathContainer fibonacci = factory2.CreateInstance();

       Console.WriteLine("Count Factorial form 1 to 8:");
       for (int i=1;i<=8;i++)
       {
              factorial.Add();
       }
       for (int i=0;i&lt;8;i++)
       {
              Console.WriteLine(factorial[i].ToString());
       }

       Console.WriteLine();
       Console.WriteLine("Count Fibonacci form 1 to 8:");

       for (int i=1;i<=8;i++)
       {
              fibonacci.Add();
       }
       for (int i=0;i&lt;8;i++)
       {
              Console.WriteLine(fibonacci[i].ToString());
       }
       Console.ReadLine();
}

本来是一个简单的例子,似乎到后来越来越复杂了。然后仔细分析程序结构,你会发现这个的扩充性和灵活性是很好的。通过重构,并运用设计模式的工厂模式,我们逐步创建了这样一个渐趋完美的数学运算容器。大家可以试试为这个容器添加其他算法。也许在这个过程中你会发现结构还存在一些不足,那么不用担心,运用重构的武器吧。虽然这个武器可能比不上CS高手手里的AK47,不过对于对付大多数问题,也足以所向披靡了。

posted @ 2004-09-14 20:36 张逸 阅读(2200) 评论(8) 编辑 收藏

 回复 引用   
#1楼 2004-09-15 02:32 JGTM'2004 [MVP][未注册用户]
文章不错,只是文中所说的“ArrayList的Add()方法不仅可以添加已经实例化或业已获得的实体对象还可以添加具体的成员方法”并不是ArrayList的什么特性罢了……这并不是你解决问题所使用的关键,你说呢?:)
 回复 引用   
#2楼 2004-09-15 09:39 柚子Nan

当我们添加一个方法时,它会首先执行该方法,然后再将该方法返回的结果存储到ArrayList中。 
如果他不执行这个方法,那么他就是把方法缓存了,不就跟递归类似了。
看了看ArrayList的Add源码如下:



public virtual int Add(object value)
{
      
if (this._size == this._items.Length)
      
{
            
this.EnsureCapacity(this._size + 1);
      }

      
this._items[this._size] = value;
      
this._version++;
      
return this._size++;
}
不过楼上的这些思想,小弟还需要慢慢的体会!每天总是忙得开发,能够静下心来思考的时间还真不多。

 回复 引用   
#3楼 2004-09-15 10:07 wayfarer
@JGTM'2004[MVP]

最初我真是将这个看成是ArrayList的特性了,当然作为这一点来说,没必要写一篇文章。由于这段时间正在看重构和设计模式,所以想顺便写篇文章,整理一下自己的某些收获。

至于我所说的特性,现在看来是闹了个笑话。因为如果我把ArrayList改为数组,也是一样可行的。
long[] array = new long[1];
array[0] = Count();

此时仍然会先执行Count()方法,再将返回值赋给array[0]。

其实不光是数组,对于很多语言,这都是可行的。我在平时忽略了,以为应该先执行方法,然后获得返回值在添加到ArrayList中。因此看到这种应用时,还以为自己捡到宝了,根本没有细想:(post的评论上,问题男已经提出这个问题了。


另外谢谢柚子楠给出了ArrayList的Add方法()的源码,这已经能说明问题了。

汗啊!

不过文章我也不准备修改了,以此为戒,以此为戒!!

还有谢谢JGTM'2004[MVP]给我打了圆场,说“这并不是你解决问题所使用的关键”。呵呵。其实写文章的初衷还真是因为这个,只是后来我“重构”到重构和设计模式上去了。

 回复 引用   
#4楼 2004-09-15 11:14 JGTM'2004 [MVP][未注册用户]
建议下一次你用TDD的方法重新做一边同样的题目,相信你会有更大的收获。:)
 回复 引用   
#5楼 2004-09-15 11:59 wayfarer
@JGTM'2004[MVP]

好的。谢谢!

 回复 引用   
#6楼 2004-09-15 12:03 wayfarer
@JGTM'2004[MVP]

其实最近正在看TDD,同时还在翻译MSDN上的一篇文章,是讲解敏捷开发和TDD的。现在翻译了一半:)

NUnit也正在熟悉中。不过为了文章的质量好些,避免出现错误,这次写得话就要慎重了:)

谢谢你的建议,呵呵,把NUnit,TDD,Refactoring,Design Pattern结合起来,想想也不错啊!

 回复 引用   
#7楼 2004-09-15 12:37 Ninputer[未注册用户]
我觉得这不是Add的特性,而是表达式本来就这样计算的。
事实上,我觉得有一种坏味道是“复杂表达式”。特别是在if、switch、lock、函数调用等等上下文中调用函数。比如

if (GetName(id) != string.Empty)
switch(GetContent(x))
lock(this.GetType())
arr.Add(Func2())

等等某种程度上都不是良好的风格。我觉得函数调用一定要放在赋值语句的右面。比如那个if也是一样
name = GetName(id);
if (name != null)

在Add后面直接放函数调用表达式同样也是不好的,应该用变量接受函数的计算结果,然后再使用Add
obj = Func2()
arr.Add(obj)


 回复 引用   
#8楼 2004-09-15 15:02 wayfarer
恩,也有道理。这其实就是说到编码风格了。

其实我最初看到Add在这样使用的时候,就大惑不解,看了很久才明白。因为我很少看到这样使用的,还以为是它的一个特性呢:(