[.net 面向对象编程基础] (14) 重构

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

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 = ;
NewMethod2("","");
} private static void NewMethod2(string s1, string s2)
{
string s = s1 + s2;
}

移除后的代码

 protected void Page_Load(EventArgs e, object sender)
{
int i = ;
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 = ;
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 = ; 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 = ; 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 = ; 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 = ;
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 = ; 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.重构是一门代码艺术。

==============================================================================================

返回目录 <如果对你有帮助,记得点一下推荐哦,有不明白的地方或写的不对的地方,请多交流>

==============================================================================================

[.net 面向对象编程基础] (14) 重构的更多相关文章

  1. [.net 面向对象编程基础] (1) 开篇

    [.net 面向对象编程基础] (1)开篇 使用.net进行面向对象编程也有好长一段时间了,整天都忙于赶项目,完成项目任务之中.最近偶有闲暇,看了项目组中的同学写的代码,感慨颇深.感觉除了定义个类,就 ...

  2. [.net 面向对象编程基础] (16) 接口

    [.net 面向对象编程基础] (16) 接口 关于“接口”一词,跟我们平常看到的电脑的硬件“接口”意义上是差不多的.拿一台电脑来说,我们从外面,可以看到他的USB接口,COM接口等,那么这些接口的目 ...

  3. [.net 面向对象编程基础] (19) LINQ基础

    [.net 面向对象编程基础] (19)  LINQ基础 上两节我们介绍了.net的数组.集合和泛型.我们说到,数组是从以前编程语言延伸过来的一种引用类型,采用事先定义长度分配存储区域的方式.而集合是 ...

  4. [.net 面向对象编程基础] (20) LINQ使用

    [.net 面向对象编程基础] (20)  LINQ使用 通过上节LINQ的基础知识的学习,我们可以开始使用LINQ来进行内存数据的查询了,我们上节说了LINQ的定义为:Language Integr ...

  5. 第二章 Matlab面向对象编程基础

    DeepLab是一款基于Matlab面向对象编程的深度学习工具箱,所以了解Matlab面向对象编程的特点是必要的.笔者在做Matlab面向对象编程的时候发现无论是互联网上还是书店里卖的各式Matlab ...

  6. [Java入门笔记] 面向对象编程基础(二):方法详解

    什么是方法? 简介 在上一篇的blog中,我们知道了方法是类中的一个组成部分,是类或对象的行为特征的抽象. 无论是从语法和功能上来看,方法都有点类似与函数.但是,方法与传统的函数还是有着不同之处: 在 ...

  7. [.net 面向对象编程基础] (2) 关于面向对象编程

    [.net 面向对象编程基础]  (2)  关于面向对象编程 首先是,面向对象编程英文 Object-Oriented Programming 简称 OOP 通俗来说,就是 针对对象编程的意思 那么问 ...

  8. [.net 面向对象编程基础] (3) 基础中的基础——数据类型

    [.net 面向对象编程基础] (3) 基础中的基础——数据类型 关于数据类型,这是基础中的基础. 基础..基础..基础.基本功必须要扎实. 首先,从使用电脑开始,再到编程,电脑要存储数据,就要按类型 ...

  9. [.net 面向对象编程基础] (4) 基础中的基础——数据类型转换

    [.net面向对象编程基础] (4)基础中的基础——数据类型转换 1.为什么要进行数据转换? 首先,为什么要进行数据转换,拿值类型例子说明一下, 比如:我们要把23角零钱,换成2.30元,就需要把整形 ...

随机推荐

  1. Daily Scrum 12.18

    对于老师课上所问为什么燃尽图(图如下)的完成小时数增加的问题,我们的理解是完成小时数是完成迭代2所需要的总共时间,当加入任务的时候,也就是蓝色部分增长的时候,完成小时数就会增加. 今日大家都在做编译实 ...

  2. Memcached Memcached.ClientLibrary.SockIOPool”的类型初始值设定项引发异常

    又一次遭遇"xxx类型初始值设定项引发异常" 下了个c#实现的轻量级IoC开源项目,可是在本地使用时发现一运行就捕捉到"类型初始值设定项引发异常"的异常信息,调 ...

  3. GDI+ 发生一般性错误解决办法

    错误的代码g对象继续占用 未释放资源 如果路径不一样 没问题 相同路径 获取图片进行 缩略会造成GDI错误 /// <summary> /// 生成缩略图 /// </summary ...

  4. MySQL数据库安装与配置详解

    转载提示:在原文http://www.cnblogs.com/sshoub/p/4321640.html基础上修改. 目录 一.概述 二.MySQL安装 三.安装成功验证 四.NavicatforMy ...

  5. Ajax Step By Step2

    第二.[$.get()和$.post()方法] .load()方法是局部方法(有需要父$),因为他需要一个包含元素的 jQuery 对象作为前缀.而$.get()和 $.post()是全局方法,无须指 ...

  6. XML文件的读写

    using System; using System.Collections.Generic; using System.Xml; namespace COMMON { public class Xm ...

  7. JSON字符串解析

    有时保存在数据库的数据是一串json字符串,需要进行读取的时候就需要解析操作. 简单介绍两种: 1.net.sf.json.* 2.com.alibaba.fastjson.* 需要的包自行下载. 第 ...

  8. 【jquery】flexiGrid新增功能的方式

    到目前为止,我用过的jquery表格插件只有jqGrid和flexiGrid.今天,只说后者. 关于参数配置,使用方法,博客园文章很多. 原生的flexiGrid架包不大,内容可以扩展,比如在每行数据 ...

  9. H5中的拖拽事件

    最近浏览了张鑫旭大神的基于HTML5 drag/drop模块拖动插入排序删除完整实例,感觉受益匪浅.于是将最做的demo记录下来. 首先浏览一下事件,这些事件比较好记,只要记住用在谁的身上就好了,无非 ...

  10. iframe的自适应

    iframe标签的应用感觉很强大,但是有的低版本好像不是很兼容,所以有的时候需要注意这个的兼容问题,iframe 元素会创建包含另外一个文档的内联框架(即行内框架),他的属性有很多,也很容易理解,就不 ...