求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
 
必会重构技巧

2010-09-26 作者:Dennis.Yang 来源:Dennis.Yang的blog

 

(一):封装集合

封装集合:将集合中的某些方法封装起来,这些方法一般会牵扯到其他的逻辑。

举例理解:比如你给一个List<T>里面加一个对象的同时,可能还有一个计数器在计算List中对象的个数,我们不用暴露计数器,这样List.Add()和List.Remove()我们就可以封装起来了。

项目实例:我记得我有个项目需要不断的从数据库中读取User的Guid然后狂发Mail。开始的想法很简单,根据Window ID 去 CorpDirectory 抓Guid和Mail Address就好了。实际情况也就这么简单,但是后来Debug的时候发现程序很慢,弄了半天终于发现是这个抓Guid和Mail的动作太慢了,CorpDirectory 这个DB在美国,Production的Server在国内,于是乎每次请求都来往返于太平洋。怎么解决?大家肯定一个就想到了Cache,恩,我第一个也是想到了在Server创建Cache。本实例的意义并不在于使用Cache来提高App的性能,而在于对于Cache内对象的存取。

先来看看原始的未经过封装的代码:

publicclass UserEntity
{
public Guid Gid { get; set; }
publicstring Name { get; set; }
}

添加Cache对象方法

privatevoid OldAddUserCache()
{
var user =
new UserEntity
{
Gid =
new Guid(),
Name =
"Yang,Dennis"
};
IDictionary<Guid,
string> users = new Dictionary<Guid, string>();
if (Cache["Users >"] != null)
{
users = Cache[
"Users"] as IDictionary<Guid, string>;
if (users != null && !users.ContainsKey(user.Gid))
{
users.Add(user.Gid, user.Name);
Cache[
"Users"] = users;
}
}
else
{
Cache[
"Users"] = users;
}
}

以上方法可以实现对Cache的添加,让我们分析一下有什么问题,我觉得至少有以下四点:

(1) 对于每次的Cache存取都要再写一遍代码,都是重复劳动不爽。

(2) 把Add,Remove暴露出来,增加手误,出现Bug的几率。

(3) 最重要的,如果要改里面的逻辑将要去改很多相似的代码段。

(4) 对于其它一些可能需要用的相似的方法,比如出了增加以外的 删除、读取等,都需要写很多相似的代码段

为了偷懒,为了使程序程序便于维护,这段代码有必要重构,封装!

经过重构的代码:

public enum ManageUserCacheActionType
{
Add =
1,
Remove =
2,
Read =
3
}

privatevoid TestManagerUserCache()
{
var user =
new UserEntity
{
Gid =
new Guid(),
Name =
"Yang,Dennis"
};
ManageUserCache(user, ManageUserCacheActionType.Add);
ManageUserCache(user, ManageUserCacheActionType.Read);
ManageUserCache(user, ManageUserCacheActionType.Remove);
ManageUserCache(user, ManageUserCacheActionType.Read);
}

管理UserCache的方法

privatevoid ManageUserCache(UserEntity user, ManageUserCacheActionType act)
{
IDictionary<Guid,
string> users = new Dictionary<Guid, string>();
if (Cache["Users"] != null)
{
users = Cache[
"Users"] as IDictionary<Guid, string>;
switch (act.ToString())
{
case"Add":
ManageUserCacheAdd(users, user);
break;
case"Remove":
ManageUserCacheRemove(users, user);
break;
case"Read":
ManageUserCacheRead(users, user);
break;
default:
return;
}
}
else
{
Cache[
"Users"] = users;
}
}

privatevoid ManageUserCache(UserEntity user, ManageUserCacheActionType act)
{
IDictionary<Guid,
string> users = new Dictionary<Guid, string>();
if (Cache["Users"] != null)
{
users = Cache[
"Users"] as IDictionary<Guid, string>;
switch (act.ToString())
{
case"Add":
ManageUserCacheAdd(users, user);
break;
case"Remove":
ManageUserCacheRemove(users, user);
break;
case"Read":
ManageUserCacheRead(users, user);
break;
default:
return;
}
}
else
{
Cache[
"Users"] = users;
}
}

对UserCache的增加、删除、读取

privatevoid ManageUserCacheRead(IDictionary<Guid, string> users, UserEntity user)
{
if (users != null && users.ContainsKey(user.Gid))
{
Response.Write(
string.Format("Guid:{0} -- Name:{1}<br>", user.Gid, user.Name));
}
else
{
Response.Write(
string.Format("Guid:{0} not exist<br>", user.Gid));
}
}
privatevoid ManageUserCacheRemove(IDictionary<Guid, string> users, UserEntity user)
{
if (users != null && users.ContainsKey(user.Gid))
{
users.Remove(user.Gid);
Cache[
"Users"] = users;
}
}
privatevoid ManageUserCacheAdd(IDictionary<Guid, string> users, UserEntity user)
{
if (users != null && !users.ContainsKey(user.Gid))
{
users.Add(user.Gid, user.Name);
Cache[
"Users"] = users;
}
}

以上重构方法包括:对集合方法的封装提取方法功能的单一化

归纳总结:

(1)把易出Bug的方法封装到类中,不要直接暴露出来,比如对于List,Dictionary,Queue等的Add(),Remove()。

(2)在本文中其实也用到了其它的重构方法,如提取方法,提取方法对象,后面会有专门一篇介绍提取的技巧。 

OK,这下清晰了,除了主函数,其它的方法可以独立出一个UserCacheUtility的类文件,以后所有对于UserCache的操作可就相当轻松了。

如此封装一下,可读性,可维护性都大大增强了,这种重构,易于掌握又很有实效,绝对是Coding必备技巧。

第一篇就写到此,真的没写过这种东西,希望各位提出宝贵意见,有砖头尽管砸过来吧:)

(二):使用多态替换条件

使用多态替换条件:指在进行类型检查和执行某些类型操作时,最好将算法封装在类中,并且使用多态来对代码中的调用进行抽象

举例理解:看定义可能比较迷糊,其实说的简单一点,对于使用分支语句并且分支条件是和类型检查相关的程序段,如 if(type == typeof(TypeA)){...}else if(type == typeof(TypeB)){...},可以把{...}中的Code,尝试放到if的条件中去。然后通过检查Type就可以直接返回需要的东东了,这样做可以利用已有的继承层次进行计算,比较便于维护。如果还是觉得说的太抽象,可以看看下面的代码感觉一下。

项目实例:用WPF做一个网游的客户端Demo,里面需要对商品,邮件,物品栏做分页操作。于是手动写了几个分页的类。开始是把分页的计算方法都写在了事件里面的,每一个Button绑定一个事件,每次需要修改或者使用分页的时候,都要找到相关类进行修改,复制,各个方法的耦合程度大增,程序可读性,复用性和可维护性都不太好。虽然这个项目是很久之前做的了,但这里既然想起来了,觉得还是可以尝试用这种重构方法,效果如何大家自己看看吧。

先来看看原始的未经过重构的代码:

原始代码

//x:目标页数索引值 y:每页显示记录个数 这里不Care你是如何取到这几个值的和对这两个Int值合法性的验证
protectedvoid btnFirstPage_Click(object sender, EventArgs e)
{
Button btn = sender
as Button;
if (btn != null)
{
repDataList.DataSource = Getdata(x,y);
//Get Date from DB
}
}
protectedvoid btnPrePage_Click(object sender, EventArgs e)
{
Button btn = sender
as Button;
if (btn != null)
{
repDataList.DataSource = Getdata(x, y);
//Get Date from DB
}
}
protectedvoid btnNextPage_Click(object sender, EventArgs e)
{
Button btn = sender
as Button;
if (btn != null)
{
repDataList.DataSource = Getdata(x, y);
//Get Date from DB
}
}
protectedvoid btnlastPage_Click(object sender, EventArgs e)
{
Button btn = sender
as Button;
if (btn != null)
{
repDataList.DataSource = Getdata(x, y);
//Get Date from DB
}
}
private DataSet Getdata(int targetPageIndex, int numberPerPage) //Get Date from DB
{
thrownew NotImplementedException(); //Here some code to retrive Data from DB
}

上面的几段代码可以实现功能,但是却存在以下几点问题。

(1)如果有多个页面需要使用分页,同样的代码需要复制多次,复用性差

(2)第一个问题造成了第二个问题,如果需要修改某段方法,那所有的相关页面都要修改,可维护性差

(3)相似逻辑的方法写在了多个事件中,可读性差

为了解决这三个问题,现在重构后的代码如下:

重构后

publicabstractclass Paging
{
publicint TargetPageIndex { get; set; }
publicint NumberPerPage { get; set; }
publicabstract DataSet DataList { get; }
}
publicclass FirstPage : Paging
{
public FirstPage(int targetPageIndex, int numberPerPage)
{
TargetPageIndex = targetPageIndex;
NumberPerPage = numberPerPage;
}
publicoverride DataSet DataList
{
get
{
return Getdata(TargetPageIndex, NumberPerPage);
}
}
}
publicclass PrePage : Paging
{
public PrePage(int targetPageIndex, int numberPerPage)
{
TargetPageIndex = targetPageIndex;
NumberPerPage = numberPerPage;
}
publicoverride DataSet DataList
{
get
{
return Getdata(TargetPageIndex, NumberPerPage);
}
}
}
publicclass NextPage : Paging
{
public NextPage(int targetPageIndex, int numberPerPage)
{
TargetPageIndex = targetPageIndex;
NumberPerPage = numberPerPage;
}
publicoverride DataSet DataList
{
get
{
return Getdata(TargetPageIndex, NumberPerPage);
}
}
}
publicclass LastPage : Paging
{
public LastPage(int targetPageIndex, int numberPerPage)
{
TargetPageIndex = targetPageIndex;
NumberPerPage = numberPerPage;
}
publicoverride DataSet DataList
{
get
{
return Getdata(TargetPageIndex, NumberPerPage);
}
}
}

以上代码可以独立出一个Paging的类,略加修改就可以对应任意的DataSource。

前台调用代码

protectedvoid btnPaging_Click(object sender, EventArgs e)
{
Button btn = sender
as Button;
if (btn == null) return;
Paging page;
switch (btn.CommandArgument)
{
case"FirstPage":
page =
new FirstPage(x,y);
repDataList.DataSource = page.DataList;
break;
case"PrePage":
page =
new PrePage(x, y);
repDataList.DataSource = page.DataList;
break;
case"NextPage":
page =
new NextPage(x, y);
repDataList.DataSource = page.DataList;
break;
case"LastPage":
page =
new LastPage(x, y);
repDataList.DataSource = page.DataList;
break;

}
}

个人感觉,对于本例中的应用,好处并不是很明显,也许是我写的有问题,也许是我选择例子有问题。尽管如此,重构的思想体现出来了,那就是把算法封装到多态中。此种重构在对于含类型判断条件的复杂算法分支的应用上,效果还是比较显著的。

(三):提取接口

提取接口:当有多余一个类使用另外一个类中的方法时,可以考虑引入接口,解除这种依赖。

举例理解:比如说类A中有个方法为Call(Type T),类B和类C中都有方法都要调用Call这个方法,那么我们推荐引入一个接口,这样传参时可以直接new一个接口,可以解除调用方法和实现方法之间的耦合关系。面向接口编程也算是OO中比较重要的吧。

项目实例:一般而言在设计的时候,对于比较可能扩展的部分都会用接口或者是抽象方法来处理,对于接口,个人并不是很喜欢,因为接口写好了要修改就很困难,只能再加新的接口,这对设计的要求很高,抽象方法相对好用点。下面的代码扩展于原文的Demo Code,希望可以讲得稍微详细点。

先来看看原始的未经过重构的代码: 

实现类

publicclass ClassRegistration
{
publicvoid CreateAdmin()
{
// create registration code
}
publicvoid CreateUser()
{
// create registration code
}

publicdecimal Total { get; privateset; }
}
publicclass RegistrationProcessor
{
publicdecimal ProcessRegistrationAdmin(ClassRegistration registration)
{
registration.CreateAdmin();
return registration.Total;
}
publicdecimal ProcessRegistrationUser(ClassRegistration registration)
{
registration.CreateUser();
return registration.Total;
}
}

调用方法

protectedvoid CreateAdmin_Click(object sender, EventArgs e)
{
RegistrationProcessor registrationProcessor =
new RegistrationProcessor();
registrationProcessor.ProcessRegistrationAdmin(
new ClassRegistration());
}

protectedvoid CreateUser_Click(object sender, EventArgs e)
{
RegistrationProcessor registrationProcessor =
new RegistrationProcessor();
registrationProcessor.ProcessRegistrationUser(
new ClassRegistration());
}

上面的伪代码实现了创建Admin和User的主要类和主要实现方法及调用事件,这样的代码看似没问题,其实是非常不方便扩展的。请大家想想,如果我现在要添加一个创建Viewer用户的事件,需要改几个方法?我整理如下:

(1)在类ClassRegistration中增加一个CreateViewer()的方法;

(2)在类RegistrationProcessor中增加一个处理注册Viewer用户的方法ProcessRegistrationViewer()

(3)在CreateViewer的Button事件中添加代码;

如上,改的地方实在太多了,这里对于创建用户的这个方法完全可以抽象出来,把它作为一个接口方法处理,重构后的代码如下:

重构后的实现类

publicinterface IClassRegistration
{
void Create();
decimal Total { get; }
}
publicclass ClassRegistrationAdmin : IClassRegistration
{
publicvoid Create()
{
// create registration code
}
publicdecimal Total { get; privateset; }
}
publicclass ClassRegistrationUser : IClassRegistration
{
publicvoid Create()
{
// create registration code
}
publicdecimal Total { get; privateset; }
}
publicclass RegistrationProcessor
{
publicdecimal ProcessRegistration(IClassRegistration registration)
{
registration.Create();
return registration.Total;
}
}

重构后的调用方法

protectedvoid Create_Click(object sender, EventArgs e)
{
var btn = sender
as Button;
if (btn != null)
{
RegistrationProcessor registrationProcessor =
new RegistrationProcessor();
IClassRegistration registration;
switch (btn.CommandArgument)
{
case"CreateAdmin":
registration =
new ClassRegistrationAdmin();
registrationProcessor.ProcessRegistration(registration);
break;
case"CreateUser":
registration =
new ClassRegistrationUser();
registrationProcessor.ProcessRegistration(registration);
break;
}
}
}

如上,提取接口后,对于新类型用户的创建就方便多了,新建一个基于接口的创建新用户的类,然后在Button的事件中加个分支就好了。并且我们只需要实现接口中的方法Create()就OK了。

面向接口编程,能大大提高程序的可扩展性可维护性,对于程序的模块化很有帮助,非常适合用于多模块,多团队合作的项目。

(四):提取工厂类

提取工厂类:使用一个简单工厂类来新建对象实例。

举例理解:对于一个客户端事件,我们可能需要初始化一个对象实例,并调用其中的几个方法做一系列的操作。如果客户端事件经常需要扩展,那可能每次初始化的对象实例可能都是不同的,那么为了把这个初始化对象的动作封装起来,为了使这个行为更加便于维护,我们就需要把初始化对象的动作交给简单工厂类来统一完成。

项目实例:做过一个小型的购物商城。其中有个需求简述如下:管理员可以通过后台自助增删改当前商品的打折比例和打折类型。一开始我们想的都很简单,以为用户仅仅是打折而已,OK,加个下拉列表然后里面放上1-9折就行了,然后售价X这个折数就好了。想不到设计好后,当时就被驳回了,客户说我们不仅仅会打折,可能还会返点。客户的需求向来是多变的,不能要求需求不变,那只有在设计上改变了。先看看现在项目中的设计吧,具体代码忘了,下面的代码大部分引自博客园上一大牛的文章,因为情况差不多,引来用一下,说明道理就好了。

另注:简单工厂应对一些简单逻辑比较适用,并不适合一些逻辑比较复杂的应用。

下面直接贴出经过重构的代码:

相关抽象类

abstractclass SuperCash
{
publicabstractdouble 0;"> ReturnTotalCash(double money);
}
class NormalCash : SuperCash
{
publicoverridedouble ReturnTotalCash(double money)
{
return money;
}
}
class RebateCash : SuperCash
{
privatedouble moneyRebate = 1d;
public RebateCash(double _mRebate)
{
moneyRebate = _mRebate;
}

publicoverridedouble ReturnTotalCash(double money)
{
return money * moneyRebate;
}
}
class ReturnCash : SuperCash
{
privatedouble moneyCondition = 0.0d;
privatedouble moneyReturn = 0.0d;
public ReturnCash(double _mCondition, double _mReturn)
{
moneyCondition = _mCondition;
moneyReturn = _mReturn;
}

publicoverridedouble ReturnTotalCash(double money)
{
if (money > moneyCondition)
{
return money - moneyReturn;
}
return money;
}
}

现金消费工厂

class CashFactory
{
publicstatic SuperCash CreateSuperCash(string type)
{
SuperCash superCash =
null;
switch (type)
{
case"Normal":
superCash =
new NormalCash();
break;
case"80% Off":
superCash =
new RebateCash(0.8);
break;
case"50% Off":
superCash =
new RebateCash(0.5);
break;
case"100 Per 300":
superCash =
new ReturnCash(300, 100);
break;
}
return superCash;
}
}

调用的事件

protectedvoid btnTotal_Click(object sender, EventArgs e)
{
string type = ddlType.SelectedItem.Value;
SuperCash superCash = CashFactory.CreateSuperCash(type);
double total = superCash.ReturnTotalCash(Convert.ToDouble(txtPrice.Text)*Convert.ToDouble(txtNum.Text));
lbTotal.Text = total.ToString();
}

看看上面的代码,仔细想想,发现为了增加一个打折,还是需要修改后台代码,修改部分如下:

(1)需要在客户前端的DropDownList中加入新打折方案

(2)需要修改Cashfactory工厂类

(3)需要增加一个对应的实现了接口的打折方法

那如何才能省掉这三步呢?如何让我们仅仅通过配置就可以搞定这一切呢?对应的修改方案如下:

(1)使用Xml代替硬编码,DropDownList读取Xml绑定数据。

(2)在工厂类中,运用反射获取,初始化并返回实例对象。

(3)如果确定了商品价格可能出现的类型,比如只有三种情况a:正常价格;b:打折;c:买多少返利多少,那么我们基本可以做到只用修改配置文件就可以实现修改用户的需求了。

下面的代码不写了,各位有兴趣可以试试。

(五):划分职责

划分职责:根据方法实现的逻辑来安排方法所在的类。

举例理解:这个重构的方法是对单一职责原则(SRP)的贯彻,在Coding的时候,我们不仅仅需要把方法中的逻辑单一化(主要使用 Extract Method),还要把类中的方法安置合理化。比如说有个Book()的类,那么对于Book的一些操作,如增加减少书,设置书的属性那可以交给这个类做;而如另一些方法,如买书,租书就可以交给Custom()的类来处理,因为买书,租书的逻辑主体都是Custom。

项目实例:就个人而言,这个重构方法我觉得大家在Coding的时候都会注意到,因为谁都会把相关的方法放在一个类中;唯一可能出现的问题就是出现大神类(God Class),也就是说这个类中集合了N多的方法,在项目中经常能看到这样的类,一个类中包含对String的处理,对Cache的处理,对数组的处理,总之是所有应用类的方法都塞在了一个类中,这样写起来方便了,但是使用起来会顺手么?找一个方法要找半天,并不适合维护。所以像这种情况我们何不把这些方法按照功能分开几个类写呢?

上面例子的相关代码:

重构前

public class Book
{
public void PayFee(decimal fee)
{
}

public void RentBook(Book book, Customer customer)
{
customer.Books.Add(book);
}

public decimal CalculateBalance(Customer customer)
{
t;return customer.LateFees.Sum();
}
}
public class Customer
{
public IList<decimal> LateFees { get; set; }
public IList<Book> Books { get; set; }
}

注意,这里Custom类中没有方法。

重构后

public class Book
{
public void RentBook(Book book, Customer customer)
{
customer.Books.Add(book);
}
}
public class Customer
{
public IList<decimal> LateFees { get; set; }
public IList<Book> Books { get; set; }

public void PayFee(decimal fee)
{
}

public decimal CalculateBalance(Customer customer)
{
return customer.LateFees.Sum();
}
}

如上,把RentBook和CalculateBalance移到了Customer类中,在这个示例中,这种重构似乎没有多大的作用,毕竟仁者见仁智者见智,很多种重构有时候看来真的挺叽歪的,并且有些重构对于程序性能的提高帮助并不大。但是,重构的目的,我个人看来,主要是培养我们的一个Coding习惯,写出做可维护的,易扩展的程序是我们Coder的责任。



如何向妻子解释OOD
OOAD与UML笔记
UML类图与类的关系详解
UML统一建模语言初学
总结一下领域模型的验证
基于 UML 的业务建模


面向对象的分析设计
基于UML的面向对象分析设计
UML + 嵌入式系统分析设计
关系数据库面向OOAD设计
业务建模与业务架构
使用用例进行需求管理


某航空IT部门 业务分析与业务建模
联想 业务需求分析与建模
北京航管科技 EA工具与架构设计
使用EA和UML进行嵌入式系统分析
全球最大的茶业集团 UML系统分析
华为 基于EA的嵌入式系统建模
水资源服务商 基于EA进行UML建模
更多...