重构(Refactoring)

1.     前言

本来不想写重构,因为VS2005的加了重构功能但目前和Resharper、Eclipse等还是没有可比性。但既然已经有了重构,那么预计VS系列将加强这方面的功能,所以还是先来体验下吧。

 

2.     什么是重构

重构是在编写代码后在不更改代码的外部行为的前提下通过更改代码的内部结构来改进代码的过程。目的是提高其可理解性,降低其修改成本。

通俗的说法就是,程序的功能和结果没有任何的变化。重构只是对程序内部结构进行调整,让代码更加容易理解,然后更容易维护。

 

3.     为什么要重构

至于为什么要重构,因本人才疏学浅,故特引用软件工程专家的一段话:

在不改变系统功能的情况下,改变系统的实现方式。为什么要这么做?投入精力不用来满足客户关心的需求,而是仅仅改变了软件的实现方式,这是否是在浪费客户的投资呢?

重构的重要性要从软件的生命周期说起。软件不同与普通的产品,他是一种智力产品,没有具体的物理形态。一个软件不可能发生物理损耗,界面上的按钮永远不会因为按动次数太多而发生接触不良。那么为什么一个软件制造出来以后,却不能永远使用下去呢?

对软件的生命造成威胁的因素只有一个:需求的变更。一个软件总是为解决某种特定的需求而产生,时代在发展,客户的业务也在发生变化。有的需求相对稳定一些,有的需求变化的比较剧烈,还有的需求已经消失了,或者转化成了别的需求。在这种情况下,软件必须相应的改变。

考虑到成本和时间等因素,当然不是所有的需求变化都要在 中实现。但是总的说来,软件要适应需求的变化,以保持自己的生命力。

这就产生了一种糟糕的现象:软件产品最初制造出来,是经过精心的设计,具有良好架构的。但是随着时间的发展、需求的变化,必须不断的修改原有的功能、追加新的功能,还免不了有一些缺陷需要修改。为了实现变更,不可避免的要违反最初的设计构架。经过一段时间以后,软件的架构就千疮百孔了。bug越来越多,越 来越难维护,新的需求越来越难实现,软件的构架对新的需求渐渐的失去支持能力,而是成为一种制约。最后新需求的开发成本会超过开发一个新的软件的成本,这就是这个软件系统的生命走到尽头的时候。

重构就能够最大限度的避免这样一种现象。系统发展到一定阶段后,使用重构的方式,不改变系统的外部功能,只对内部的结构进行重新的整理。通过重构,不断的调整系统的结构,使系统对于需求的变更始终具有较强的适应能力。

通过重构可以达到以下的目标:

  ·持续偏纠和改进软件设计

重构和设计是相辅相成的,它和设计彼此互补。有了重构,你仍然必须做预先的设计,但是不必是最优的设计,只需要一个合理的解决方案就够了,如果没有重构、 会逐渐腐败变质,愈来愈像断线的风筝,脱缰的野马无法控制。重构其实就是整理代码,让所有带着发散倾向的代码回归本位。

  ·使代码更易为人所理解

  Martin Flower在《重构》中有一句经典的话:"任何一个傻瓜都能写出 可以理解的程序,只有写出人 易理解的程序才是优秀的程序员。"对此,笔者感触很深,有些程序员总是能够快速编写出可运行的代码,但代码中晦涩的命名使人晕眩得需要紧握坐椅扶手,试想一个新兵到来接手这样的代码他会不会想当逃兵呢?

  软件的生命周期往往需要多批程序员来维护,我们往往忽略了这些后来人。为了使代码容易被他人理解,需要在实现软件功能时做许多额外的事件,如清晰的排版布局,简明扼要的注释,其中命名也是一个重要的方面。一个很好的办法就是采用暗喻命名,即以​ 现的功能的依据,用形象化或拟人化的手法进行命名,一个很好的态度就是将每个代码元素像新生儿一样命名,也许笔者有点命名偏执狂的倾向,如能荣此雅号,将深以此为幸。

  对于那些让人充满迷茫感甚至误导性的命名,需要果决地、大刀阔斧地整容,永远不要手下留情!

  ·帮助发现隐藏的代码缺陷

  孔子说过:温故而知新。重构代码时逼迫你加深理解原先所写的代码。笔者常有写下程序后,却发生对自己的程序逻辑不甚理解的情景,曾为此惊悚过,后来发现这种症状居然是许多程序员常患的"感冒"。当你也发生这样的情形时,通过重构代码可以加深对原设计的 理解,发现其中的问题和隐患,构建出更好的代码。

  ·从长远来看,有助于提高编程效率

  当你发现解决一个问题变得异常复杂时,往往不是问题本身造成的,而是你用错了方法,拙劣的设计往往导致臃肿的编码。

  改善设计、提高可读性、减少缺陷都是为了稳住阵脚。良好的设计是成功的一半,停下来通过重构改进设计,或许会在当前减缓速度,但它带来的后发优势却是不可低估的。

 

4.     何时使用重构

新官上任三把火,开始一个全新的项目时,程序员往往也会燃起三把火:紧锣密鼓、脚不停蹄、加班加点,一支声势浩大的千军万"码"夹裹着程序员激情和扣击键盘的鸣金奋力前行,势如破竹,攻城掠地,直指"黄龙府"。

  开发经理是这支浩浩汤汤代码队伍的统帅,他负责这支队伍的命运,当齐恒公站在山顶上看到管仲训练的 队伍整齐划一地前进时,他感叹说"我有这样一支军队哪里还怕没有胜利呢?"。但很遗憾,你手中的这支队伍原本只是散兵游勇,在前进中招兵买马,不断壮大, 所以队伍变形在所难免。当开发经理发觉队伍变形时,也许就是克制住攻克前方山头的诱惑,停下脚步整顿队伍的时候了。

  Kent Beck提出了"代码坏味道"的说法,和我们所提出的"队伍变形"是同样的意思,队伍变形的信号是什么呢?以下列述的代码症状就是"队伍变形"的强烈信号:

  ·代码中存在重复的代码

  中国有118 家整车生产企业,数量几乎等于美、日、欧所有汽车厂家数之和,但是全国的年产量却不及一个外国大汽车公司的产量。重复建设只会导致效率的低效和资源的浪费。

  程序代码更是不能搞重复建设,如果同一个类中有相同的代码块,请把它提炼成类的一个独立方法,如果不同类中具有相同的代码,请把它提炼成一个新类,永远不要重复代码。

  过大的类和过长的方法

  过大的类往往是类抽象不合理的结果,类抽象不合理将降低了代码的复用率。方法是类王国中的诸侯国, 诸侯国太大势必动摇中央集权。过长的方法由于包含的逻辑过于复杂,错误机率将直线上升,而可读性则直线下降,类的健壮性很容易被打破。当看到一个过长的方 法时,需要想办法将其划分为多个小方法,以便于分而治之。

  牵一毛而需要动全身的修改

  当你发现修改一个小功能,或增加一个小功能时,就引发一次代码地震,也许是你的设计抽象度不够理想,功能代码太过分散所引起的。

  类之间需要过多的通讯

  A类需要调用B类的过多方法访问B的内部数据,在关系上这两个类显得有点狎昵,可能这两个类本应该在一起,而不应该分家。

  过度耦合的信息链

  "计算机是这样一门科学,它相信可以通过添加一个​ 解决任何问题",所以往往中间层会被过多地追加到程序中。如果你在代码中看到需要获取一个信息,需要一个类的方法调用另一个类的方法,层层挂接,就象输油管一样节节相连。这往往是因为衔接层太多造成的,需要查看就否有可移除的中间层,或是否可以提供更直接的调用方法。

  各立山头干革命

  如果你发现有两个类或两个方法虽然命名不同但却拥有相似或相同的功能,你会发现往往是因为开发​​ ​​成员协调不够造成的。笔者曾经写了一个颇好用的字符串处理类,但因为没有及时通告团队其他人员,后来发现项目中居然有三个字符串处理类。革命资源是珍贵的,我们不应各立山头干革命。

  不完美的设计

  在笔者刚完成的一个比对报警项目中,曾安排阿朱开发报警模块,即通过Socket向指定的短信平台、语音平台及​  警器插件发送报警报文信息,阿朱出色地完成了这项任务。后来用户又提出了实时比对的需求,即要求第三方系统以报文形式向比对报警系统发送请求,比对报警系 统接收并响应这个请求。这又需要用到Socket报文通讯,由于原来的设计没有将报文通讯模块独立出来,所以无法复用阿朱开发的代码。后来我及时调整了这个设计,新增了一个报文收发模块,使系统所有的对外通讯都复用这个模块,系统的整体设计也显得更加合理。

  每个系统都或多或少存在不完美的设计,刚开始可能注意不到,到后来才会慢慢凸显出来,此时唯有勇于更改才是最好的出路。

  缺少必要的注释

  虽然许多​ 的 书籍常提醒程序员需要防止过多注释,但这个担心好象并没有什么必要。往往程序员更感兴趣的是功能实现而非代码注释,因为前者更能带来成就感,所以代码注释 往往不是过多而是过少,过于简单。人的记忆曲线下降的坡度是陡得吓人的,当过了一段时间后再回头补注释时,很容易发生"提笔忘字,愈言且止"的情形。

  曾在网上看到过微软的代码注释,其详尽程度让人叹为观止,也从中体悟到了微软成功的一个经验。

 

5.     如何使用重构

VS.NET 2005中包括一个菜单"重构",你可以用它来实现一些常见的重构任务。下图显示出这个重构菜单和它的菜单项。重构菜单

VS.NET 2005进行重构的方法有以下几个:

• 重构类型

<1>. 提取方法

<2>. 重命名

<3>. 封装字段

<4>. 提取接口

<5>. 将局部变量提升为参数

<6>. 移除参数

<7>. 重新排列参数

【1】• 提取方法

<1>. 可以通过从现有成员的代码块中提取选定的代码来创建新方法.

<2>. 创建的新方法中包含选定的代码,而现有成员中的选定代码被替换为对新方法的调用.

<3>. 代码段转换为其自己的方法,使您可以快速而准确地重新组织代码,以获得更好的重用和可靠性.

• 优点

<1>. 通过强调离散的可重用方法鼓励最佳的编码做法。

<2>. 鼓励通过较好的组织获得自记录代码。当使用描述性名称时,高级别方法可以像读取一系列注释一样进行读取。

<3>. 鼓励创建细化方法,以简化重载。

<4>. 减少代码重复.

• 实例

当你编写了一个代码很长的方法,它包含一些非常复杂的算法集合。在完成该方法以后,你可能意识到它变得太大和太复杂了,以至于其它小组成员无法容易地理解它。因此,你决定把它拆分成多个小函数。这不仅会简化你的代码而且还能够改进其易读和可维护性。"重构"菜单下的 "提取方法"选项正是适合这一工作。

    假设方法为

    public void TestMethod()

    {

        string s = "";

}

(1)              用选中 string s = "";  然后点击"提取方法"菜单选项。立即出现"提取方法"对话框。

(2)               按“确定”,结果如下:

    public void TestMethod()

    {

        NewMethod();

    }

 

    private static void NewMethod()

    {

        string s = "";

}

重构为我们完成了两件事情:

· 它根据你的每一次选择创建一个新的方法并且替换其中的所有选择的代码。

· 它用一个到这个新创建方法的调用来替换选择的行。

【2】• 重命名

<1>. 提供了一种重命名代码符号(如字段、局部变量、方法、命名空间、属性和类型)标识符的简单方法.

<2>. “重命名”功能除了可用来更改标识符的声明和调用以外,还可用来更改注释中和字符串中的名称.

• 在何处可以使用重命名操作?

<1>. 代码编辑器

<2>. 类视图

<3>. 对象浏览器

<4>. Windows 窗体设计器的“属性网格”

<5>. 解决方案资源管理器

 

• 重命名执行些什么操作?

<1>. 字段

• 将字段的声明和用法更改为新名称。

<2>. 局部变量

• 将变量的声明和用法更改为新名称。

<3>. 方法

• 将方法的名称以及对该方法的所有引用更改为新名称。

<4>. 命名空间

• 将声明、所有正在使用的语句及完全限定名称中的命名空间名称更改为新名称。

<5>. 属性

• 将属性的声明和用法更改为新名称。

<6>. 类型

• 将类型的所有声明和所有用法都更改为新名称,包括构造函数和析构函数。对于部分类型,重命名操作将传播到其所有部分。

• 实例

假设我们需要“NewMethod”重命名为“NewMethod2”。

选中 “NewMethod” 然后点击"重命名"菜单选项。立即出现"重命名"对话框。

结果:

    public void TestMethod()

    {

        NewMethod2();

    }

 

    private static void NewMethod2()

    {

        string s = "";

    }

【3】• 封装字段

<1>. 可以从现有字段快速创建属性,然后使用对新属性的引用无缝更新代码.

<2>. 当某个字段为public(C# 参考)时,其他对象可以直接访问该字段并对其进行修改,而不会被拥有该字段的对象检测到。通过使用属性(C# 编程指南)封装该字段,可以禁止对字段的直接访问。

<3>. 仅当将光标与字段声明置于同一行时,才可以执行“封装字段”操作。

• 实例

大部分开发者都习惯把类级的变量(字段)暴露给外界。由于每一个对象都属于面向对象编程,所以开发者应该允许通过属性或方法来存取变量。这种情况可以使用重构菜单下的"封装字段"选项来进行处理。

为此,选择你想包装在一个属性中的类级变量并且选择"封装字段"选项。这将打开一个如下图所示的对话框:

你需要输入该属性的名字并且决定是否你想从类外或类内部更新到该变量的参考。就象"重命名"对话框一样,你可以在应用之前先预览一下所作的改变。

原代码:

string s;

 

封装后代码:

string s;

public string S

{

        get { return s; }

        set { s = value; }

}

 

【4】• 提取接口

<1>. 使用来自现有类、结构或接口的成员创建新接口的简单方法.

<2>. 当几个客户端使用类、结构或接口中成员的同一子集时,或者当多个类、结构或接口具有通用的成员子集时,在接口中嵌入成员子集将很有用.

<3>. 仅当将光标定位于包含要提取成员的类、结构或接口中时,才可以访问此功能。当光标处于此位置时,调用“提取接口”重构操作.

• 实例

 

 

【5】• 将局部变量提升为参数

<1>. 提供一种简单的方法,以在正确更新调用站点的同时将变量从局部使用移动至方法、索引器或构造函数参数.

<2>. 调用“将局部变量提升为参数”操作时,变量将被添加到成员参数列表的结尾处.

<3>. 对已修改成员的所有调用都将使用新参数(将替代最初赋给该变量的表达式)立即进行更新,并保留代码,以使其像变量提升之前那样正常工作.

<4>. 将常数值赋值给提升的变量时,此重构操作效果最好。必须声明并初始化该变量,而不能仅声明或仅赋值.

• 实例

原代码:

    private static void NewMethod2()

    {

        string s = "";

    }

选中s,转换后

    private static void NewMethod2(string s)

    {

 

    }

 

【6】• 移除参数

<1>. 从方法、索引器或委托中移除参数的简单方法.

<2>. 在调用成员的任何位置,都会将参数移除以反映新声明.

• 实例

原代码

    protected void Page_Load(EventArgs e, object sender)

    {

        int i = 0;

        NewMethod2("1","2");

    }

 

    private static void NewMethod2(string s1, string s2)

    {

        string s = s1 + s2;

    }

移除后的代码

    protected void Page_Load(EventArgs e, object sender)

    {

        int i = 0;

        NewMethod2();

    }

 

    private static void NewMethod2()

    {

        string s = s1 + s2;

    }

 

【7】• 重新排列参数

<1>. 对方法、索引器和委托的参数顺序进行更改的简单方法.

<2>. 可以通过方法声明或方法调用来重新排列参数。要将光标置于方法声明或委托声明中,而不是置于正文中。

• 实例

原代码:

    private static void NewMethod2(string s1,string s2)

    {

    }

重新排列后

    private static void NewMethod2(string s2,string s1)

    {

    }