重构

通过面向对象三大特性:封装、继承、多态的学习,可以说我们已经掌握了面向对象的核心。接下来的学习就是如何让我们的代码更优雅、更高效、更易读、更易维护。当然了,这也是从一个普通程序员到一个高级程序员的必由之路。就看病一样,普通医生只能治标,高级医生不但看好病,还能除病根。

1.什么时重构?

重构( Refactoring )就是在不改变软件现有功能的基础上,通过调整程序代码改善软件的质量、性能,使其程序的设计模式和架构更趋合理,提高软件的扩展性和维护性。

目的:是提高其可理解性 , 降低其修改成本。

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

通过上述定义,可以看出,重构并不是 .net 的本身的特性,而是软件设计范畴。

2.重构的目的

A. 改进软件的设计

在实际工作中,为了赶进度或是为了短期利益,再或者是没有完全摸清软件整体架构的情况下,对代码进行改动。而这些改动的积累很容易使软件偏离它原先的设计初衷,使软件变很很难维护或无法维护。

而重构可以帮助重新组织代码,重新清晰的体现结构和进一步改进设计。

B. 提高代码的质量和可维护性

容易理解的代码很容易维护和做进一步开发。即使写这些代码的程序员本身而言,容易更解的代码也能帮助他容易的修改。

代码也是文档,首先是写给人看的,其次才是计算机。

C. 帮助尽早的发现错误

重构是一个复习和反馈的过程,在另一个时段重新审视自己或别人的代码,可以更容易发现问题和加深对代码的理解 .

重构是一个良好的开发习惯。

D. 可以提高开发速度

重构对设计和代码的改进,都可以有效提高开发速度。

在一个有缺陷的设计和混乱的代码基础上开发,即使表面是进度较快,但本质是延后对设计缺陷的发现和对错误的修改。也就延后了开发风险,最终要在开发后期付出更多的代价。

一句话,出来混,迟早是要还的!!

3.重构的时机

重构的时候,即何时需要重构,何时不需要

A. 首先,以下几种情况需要重构:

过大的类和过长的方法

过长的方法由于包含的逻辑过于复杂,错误机率将直线上升,而可读性则直线下降,类的健壮性很容易被打破。当看到一个过长的方 法时,需要想办法将其划分为多个小方法,以便于分而治之。

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

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

类之间需要过多的通讯

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

过度耦合的信息链

如果你在代码中看到需要获取一个信息,需要一个类的方法调用另一个类的方法,层层挂接,就象输油管一样节节相连。这往往是因为衔接层太多造成的,需要查看就否有可移除的中间层,或是否可以提供更直接的调用方法。

各自为政的功能模块

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

不完美的设计

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

缺少必要的注释

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

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

(以上关于重构的内容来自网上小伙伴的分析,还是比较全面的,摘录过来分享之)

B. 还有几种情况是不适用重构的:

代码混乱,错误百出,这种情况,不是重构而是需要重写了

大型多模块软件,需要逐步重构,不是一下子完成

重构需要太长的时间,这种情况下不建议重构。

项目即将进入交付阶段,隐定性胜过其它。

3.如何进行重构

前面讲了太多的理论知识,下面来点硬货,说说重构的方法。

3.1 使用 VS.NET 自身的功能实现快速重构

VS.net 本身关于重构的功能,可能很多人很少用到,作为一个重构的辅助功能,虽说不能完全实现重构,但是可以帮助我们快速优化代码。

3.1.1 重构类型

<1>. 重命名

<2>.提取方法

<3>. 封装字段

<4>. 提取接口

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

<6>. 移除参数

<7>. 重新排列参数

VS.NET 中提供了这么七种重构的类型。我们在代码编辑窗口中,点击鼠标右键,可以看到如下图所示:

下面,我们逐一说明

<1> 重命名

我们在代码重构过程中,会有不按规范命名的情况发生或者我们想让一段代码产生一个副本。

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

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

如下图所示,选中一个名称后,输入新名称, VS.NET 会提示你更改那些名字。

<2>. 提取方法

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

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

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

• 优点

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

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

C. 鼓励创建细化方法,以简化重载。

D. 减少代码重复 .

如下图,我们选中一个方法中的代码片段,点重构中的 “提取方法”弹出下下对话框,我们重命名一个新的方法名

确定后,如下所示:

生成一个静态的方法。在一个方法实现中代码片段太长的时候,我们可以很方便的进行方法提取了。

<3>. 封装字段

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

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

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

• 实例

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

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

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

如下图所示,假如我们要在动物这个类中,加一个属性,我们使用封装字段,

如果选择“外部”确定后,代码如下:

可以看到,为我们自动增加了一个外部属性

<4> • 提取接口

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

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

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

如下图所示,我们在类名称点击右键 重构,选择提取接口,在弹出窗口中,输入接口名称,选择类的公有成员,则为它们创建了一个接口文件,非常实用。

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

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

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

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

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

• 实例

原代码:

private static void NewMethod2()
{
        string s = "";
}

选中 s, 转换后

private static void NewMethod2(string s)
{ 
}

<6> • 移除参数

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

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

• 实例

原代码

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> • 重新排列参数

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

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

• 实例

原代码:

private static void NewMethod2(string s1,string s2)
{
}

重新排列后

private static void NewMethod2(string s2,string s1)
{

}

4.重构实例

我们通过一个实例来看看重构带来的好处,还是我们前一节的关于动物叫的例子,有一个基类 动物( Animal )有成员属性名字( Name )

方法叫声( Shout )和叫的次数的虚方法 (getShoutCount), 它有 N 个派生类,我们先看重构前的代码如下:

/// <summary>
/// 动物类(父类)
/// </summary>
class Animal
{
    /// <summary>
    /// 名字
    /// 说明:类和子类可访问
    /// </summary>
    protected string name;


    /// <summary>
    /// 构造函数
    /// </summary>
    /// <param name="name"></param>
    public Animal(string name)
    {
        this.name = name;
    }

    private int shoutNum = 3;
    public int ShoutNum
    {
        get { return shoutNum; }
        set { shoutNum = value; }
    }

    /// <summary>
    /// 名字(虚属性)
    /// </summary>
    public virtual string MyName
    {
        get { return this.name; }

    }

    /// <summary>
    /// 叫(虚方法)
    /// </summary>
    public virtual void Shout()
    {
        Console.WriteLine("我会叫!");
    }

}

/// <summary>
/// 狗(子类)
/// </summary>
class Dog : Animal
{
    string myName;
    public Dog(string name)
        : base(name)
    {
        myName = name;
    }

    /// <summary>
    /// 名字(重写父类属性)
    /// </summary>
    public override string MyName
    {
        get { return "我是:狗狗,我叫:" + this.name; }
    }

    /// <summary>
    /// 叫(重写父类方法)
    /// </summary>
    public override void Shout()
    {
        string result = "";
        for (int i = 0; i < ShoutNum; i++)
            result += "汪!";
        Console.WriteLine(result);
    }
}
/// <summary>
/// 猫(子类)
/// </summary>
class Cat : Animal
{
    string myName;
    public Cat(string name)
        : base(name)
    {
        myName = name;
    }
    /// <summary>
    /// 名字(重写父类属性)
    /// </summary>
    public override string MyName
    {
        get { return "我是:猫咪,我叫:" + this.name; }

    }

    /// <summary>
    /// 叫(重写父类方法)
    /// </summary>
    public override void Shout()
    {
        string result = "";
        for (int i = 0; i < ShoutNum; i++)
            result += "喵!";
        Console.WriteLine(result);
    }
}

/// <summary>
/// 羊(子类)
/// </summary>
class Sheep : Animal
{
    string myName;
    public Sheep(string name)
        : base(name)
    {
        myName = name;
    }
    /// <summary>
    /// 名字(重写父类属性)
    /// </summary>
    public override string MyName
    {
        get { return "我是:羊羊,我叫:" + this.name; }

    }

    /// <summary>
    /// 叫(重写父类方法)
    /// </summary>
    public override void Shout()
    {
        string result = "";
        for (int i = 0; i < ShoutNum; i++)
            result += "咩!";
        Console.WriteLine(result);
    }
}

我们可以看到,虽然这段代码实现了继承和多态,封装的特性,代码还是比较简洁的,但是有一点就是这个叫的方法,每个子类中都要写一次循环。假如又来了猪啊,牛啊,这些动物,是不是代码量也不少啊。我们能不能只写一次循环呢,答案是肯定的,看我们重构后的代码:

/// <summary>
/// 动物类(父类)
/// </summary>
class Animal
{
    /// <summary>
    /// 名字
    /// 说明:类和子类可访问
    /// </summary>
    protected string name;

    /// <summary>
    /// 构造函数
    /// </summary>
    /// <param name="name"></param>
    public Animal(string name)
    {
        this.name = name;
    }

    private int shoutNum = 3;
    public int ShoutNum
    {
        get { return shoutNum; }
        set { shoutNum = value; }
    }

    /// <summary>
    /// 名字(虚属性)
    /// </summary>
    public virtual string MyName
    {
        get { return this.name; }

    }

    /// <summary>
    /// 叫声,这个方法去掉虚方法,把循环写在这里
    /// </summary>
    public void Shout()
    {
        string result = "";
        for (int i = 0; i < ShoutNum; i++)
            result += getShoutSound()+"!";

        Console.WriteLine(MyName);
        Console.WriteLine(result);
    }
    /// <summary>
    /// 创建一个叫声的虚方法,子类重写
    /// </summary>
    /// <returns></returns>
    public  virtual string  getShoutSound()
    {
        return "";
    }
}

/// <summary>
/// 狗(子类)
/// </summary>
class Dog : Animal
{
    string myName;
    public Dog(string name): base(name)
    {
        myName = name;
    }
    /// <summary>
    /// 名字(重写父类属性)
    /// </summary>
    public override string MyName
    {
        get { return "我是:狗狗,我叫:" + this.name; }
    }
    /// <summary>
    /// 叫(重写父类方法)
    /// </summary>
    public override string getShoutSound()
    {
        return "汪!";
    }
}
/// <summary>
/// 猫(子类)
/// </summary>
class Cat : Animal
{
    string myName;
    public Cat(string name): base(name)
    {
        myName = name;
    }
    /// <summary>
    /// 名字(重写父类属性)
    /// </summary>
    public override string MyName
    {
        get { return "我是:猫咪,我叫:" + this.name; }
    }
    /// <summary>
    /// 叫(重写父类方法)
    /// </summary>
    public override string getShoutSound()
    {
        return "喵!";
    }
}

/// <summary>
/// 羊(子类)
/// </summary>
class Sheep : Animal
{
    string myName;
    public Sheep(string name): base(name)
    {
        myName = name;
    }
    /// <summary>
    /// 名字(重写父类属性)
    /// </summary>
    public override string MyName
    {
        get { return "我是:羊羊,我叫:" + this.name; }
    }
    /// <summary>
    /// 叫(重写父类方法)
    /// </summary>
    public override string getShoutSound()
    {
        return "咩!";
    }
}

这样重构,是不是代码量就少很多了,结构也更加清晰了。。

调用一:

//调用
Animal sheep = new Sheep("美羊羊");
sheep.Shout();
Console.ReadLine();

结果如下:

//调用结果
//我是:羊羊,我叫:美羊羊
//咩!咩!咩!

调用二:

//调用
Animal dog= new Dog("旺财");
dog.Shout();
Console.ReadLine();

结果如下:

//调用结果
//我是:狗狗,我叫:旺财
//汪!汪!汪!

总结:重构是一门复杂的学问,本节内容只是重构的皮毛而已,有一些书籍用几千页的篇幅来介绍中重构。能否熟练使用重构,写出优雅高效的代码是区分一个程序员优秀的标准之一,重构也是学习设计模的基础,这需要我们不断的练习和思考才能做好。

要点:

A.重构( Refactoring )就是在不改变软件现有功能的基础上,通过调整程序代码改善软件的质量、性能,使其程序的设计模式和架构更趋合理,提高软件的扩展性和维护性。

B.重构不是 .NET 面向对象本身的特性,而属于一种软件设计范畴。

C.重构提高了代码的可读性,可维护性;也使得代码结构更加清晰。

D.能否有效的重构代码,是一个程序员优秀与否的标准之一。也是学习设计模式和软件架构的基础。

E. 重构是一门代码艺术。