随笔 - 181  文章 - 15  trackbacks - 0
<2007年7月>
24252627282930
1234567
891011121314
15161718192021
22232425262728
2930311234

常用链接

留言簿(1)

随笔分类

随笔档案

My Tech blog

搜索

  •  

最新评论

阅读排行榜

评论排行榜

代码的坏味道
3、Large Class(过大类)
如果想利用单一class做太多事情,其内往往就会出现太多instance变量。一旦如此,Duplicated Code也就接踵而至了。
解决办法:
(1)Extract Class:将数个变量一起提炼至新class内,提炼时应该选择彼此相关的变量。
当某个class做了应该由两个class做的事的时候使用。
建立一个新的class,将相关的值域和函数从旧class搬移到新class.
步骤:
1)决定如何分解class所负责任.
2)建立一个新class,用以表现从旧class分离出来的责任.
3)建立从旧class访问新class的连接关系.
4)对于你想搬移的每一个值域,用Move Field搬移之.
5)每次搬移后,编译,测试.
6)使用Move Method将必要的函数搬移到新class,先搬移较低层次函数,即被调用次数多余调用其他函数次数的函数.再搬移较高层次的函数.
7)每次搬移之后,编译,测试.
8)检查,精简每个class接口.
9)决定是否让新class曝光.

为了加深对于重构的理解,我开始寻找能够实践这些方法的代码.终于,我找到了大学时期写的有关图形处理的一个小程序,并打算对这个程序进行重构.从某些方面讲,它还是比较合适的:
1、程序写得很烂,但是还能工作;
2、包含了大部分书中提到的“坏味道”。
3、有关位图等图形文件的格式信息和相关的规约还有算法,我已经忘得差不多了,除了知道这些代码是我自己写得之外,我已经基本上不认识它们了。这样我就可以实践书中所说的“边重构,边理解”的过程了。
4、我写这个程序的时候还比较勤快,写了不少注释。
首先,我找到最长的那个类,并绘制了类图:

这里面成员变量够多。其中有一些只不过是声明了一下;还有一些初始化了,但一次也没有使用过。
首先用这个类来实践Extract Class的重构方法。
首先,我把其中有关直方图的成员和方法独立出来,形成直方图类,以变成下面这种形式:

虽然很丑陋,但是还是先看一下在类CBasicBmpProcess中的InitHistoGram()方法:
void CBasicBmpProcess::InitHistoGram()//初始化直方图
{
    
long i;
    
int j,BmpSize=bi.biWidth*bi.biHeight,ColorCount=bitCount<<8;
    
if(ColorCount<=256)
    {
        
for(i=0;i<ColorCount;i++)
        {
            HistoGram[i]
=0.0;
            iHistoGram[i]
=0;
        }
        
for(i=0;i<BmpSize;i++)
        {
            iHistoGram[lpBuf[i]]
++;
        }
        
for(i=0;i<ColorCount;i++)
        {
               HistoGram[i]
=((float)iHistoGram[i])/BmpSize;
        }
    }
    
else
    {
        
for(i=0;i<ColorCount;i++)
        {
            HistoGramExt[i]
=0.0;
            iHistoGramExt[i]
=0;
        }
        
for(i=0;i<BmpSize-bitCount;i=i+bitCount)
        {
            
for(j=0;j<bitCount;j++)
            {
                iHistoGramExt[j
*ColorCount/bitCount+lpBuf[i+j]]++;
            }
        }
        
for(i=0;i<ColorCount;i++)
        {
               HistoGramExt[i]
=10*((float)iHistoGramExt[i])/(BmpSize/bitCount);
        }
    }

}
现在要把它抽取到新类CHistoGram中去。像下面这样:
void CHistoGram::InitHistoGram(int BmpSize,int ColorCount,BYTE *lpBuf,int bitCount)
{
    
long i=0,j=0;
    
if(ColorCount<=256)
    {
        
for(i=0;i<ColorCount;i++)
        {
            HistoGram[i]
=0.0;
            iHistoGram[i]
=0;
        }
        
for(i=0;i<BmpSize;i++)
        {
            iHistoGram[lpBuf[i]]
++;
        }
        
for(i=0;i<ColorCount;i++)
        {
               HistoGram[i]
=((float)iHistoGram[i])/BmpSize;
        }
    }
    
else
    {
        
for(i=0;i<ColorCount;i++)
        {
            HistoGramExt[i]
=0.0;
            iHistoGramExt[i]
=0;
        }
        
for(i=0;i<BmpSize-bitCount;i=i+bitCount)
        {
            
for(j=0;j<bitCount;j++)
            {
                iHistoGramExt[j
*ColorCount/bitCount+lpBuf[i+j]]++;
            }
        }
        
for(i=0;i<ColorCount;i++)
        {
               HistoGramExt[i]
=10*((float)iHistoGramExt[i])/(BmpSize/bitCount);
        }
    }
}
然后再将CBasicBmpProcess中的InitHistoGram方法变为下面这样:
void CBasicBmpProcess::InitHistoGram()//初始化直方图
{
    
int BmpSize=bi.biWidth*bi.biHeight,ColorCount=bitCount<<8;
    
this->_histoGram.InitHistoGram(BmpSize,ColorCount,lpBuf,bitCount);

}
然后继续使用Extract Class对CBasicBmpProcess进行抽离。在此过程当中,由于CBasicBmpProcess已经失去了最初的功能,所以将它重命名为CBmpData。效果如下图所示:

调用举例:
void CBmpData::AveStrainWaves()//均值滤波
{
    
this->processor->AveStrainWaves();
    BmpStatsChanged
=TRUE;
}    



(2)Extract SubClass
class  中的某些特性只被某些(而非全部)实体用到。
Extract Class和Extract Sub Class是重构的两种选择,两者之间的抉择实际上就是委托和继承之间的抉择。Extract Sub Class通常更容易进行,但它也有限制:一旦对象创建完成,你无法再改变与“型别相关的行为”。但如果使用Extract Class,你只需要插入另外一个不同组件,就可以改变对象行为。此外,subclass只能表现一组变化,如果你希望class以数种不同的方式变化,就必须使用委托。 
作法:
为Source Class定义一个新的Sub Class;
为这个新的Sub Class定义构造函数:
      简单的做法是:让Sub Class构造函数与Super Class有相同的参数,并通过Super调用Super Class构造函数.
      如果你希望对用户隐藏Sub Class的存在,可以使用Replace Constructor With Factory Method.
找出调用Super Class的所有地点,如果它们需要的是新建的Sub Class,让它们改而调用新的构造函数;
      如果Sub Class的构造函数和super class构造函数的参数不同,可以使用Rename Method修改其参数列。如果Sub Class构造函数不需要Super Class构造函数的某些参数,可以使用Rename Method将它们去除。
      (Rename Method:这里我的理解是:需要创建具体子类实例的时候,用一个名称清晰的函数去创建它,并隐藏因为构造函数不同而导致实例化时的差别。)
      如果不在需要直接实体化,就将它声明为抽象类。
逐步使用Push Down Method和Push Down Field将Source Class特性移到Sub Class中去。
      和Extract Class不同的是,先处理函数再处理数据,通常会简单一些。
      当一个public函数被下移到Sub Class后,你可能需要重新定义该函数的调用端的局部变量或参数型别,让它们调用SubClass中的新函数,如果忘记进行这一步骤,编译器会提醒你。
找到所有这样的值域:它们所传达的信息如今可由继承体系自身传达,以Self Encapsulate Field避免直接使用这些值域,然后将它们的取值函数替换为多态常量函数,所有使用这些值域的地方都应该以Replace Conditional with Polymorphism重构。
      (多态常量函数:会在不同的subclass版本中返回不同的固定值)

在我的图像处理程序中,我发现图像处理器类(CBmpProcessor)中存在一些可以分离的特性。比如均值滤波绝对不会和索贝尔边缘检测扯上什么关系,所以我打算对Processor类使用Extract Sub Class方法。让它变为下面这种形式:

然后把Process变为一个纯虚函数,这样,CBmpProcessor就成为一个抽象类了,同时还需要对调用方--即类CBasicBmpData进行修改,另外由于这个修改改动了CBasicBmpProcess暴露的公共方法,所以要调用这些方法的窗口中的事件处理函数也需要进行一定修改。首先CBmpProcessor看上去会是这个样子:

这样在SetBmpProcessor的函数体中配置Processor:
void CBmpData::SetBmpProcessor(CBmpProcessor *processor)
{
 this->processor=processor;
 this->processor->SetBitmapInfoHeader(bi);
 this->processor->SetRawData(lpBuf);
 this->processor->PrepareHistoGram(this->_histoGram);
 this->processor->PrepareProcessingData(this->lptmpBuf);
}
而调用Processor处理图像就更简单了:
void CBmpData::ProcessByProcessor(void)
{
    
this->processor->Process();
}
然后把窗口中的事件处理函数修改一下:
void CMainFrame::OnSobel() 
{
    
// TODO: Add your command handler code here
    if(WindowCounter>0)
    {
        CMDIChildWnd
* pMDIActive = MDIGetActive();
        CImageDoc
* pDoc=(CImageDoc*)pMDIActive->GetActiveDocument();
        pDoc
->bmp.SetBmpProcessor(new CSobel());
        pDoc
->
bmp.ProcessByProcessor();
        pDoc->UpdateAllViews(NULL,0,NULL);
    }
    
else
        AfxMessageBox(
"请先打开一幅位图!");
    
}

完整的CBmpProcessor类图:



(3)Extract Interface
若干客户使用class接口中的同一子集;或者两个classes的接口有部分相同。
处理方法:将相同的子集提炼到一个独立的接口中。
在许多面向对象语言中,这种“责任划分能力”是通过多重继承支持的,你可以针对一段行为建立一个class,再将它们组合于一份实现品中。java只提供单一继承,但你可以运用interface来昭示并实现这种需求。
如果某个class在不同环境中扮演截然不同的角色,使用interface是一个好主意。
作法:
新建一个空接口;
在接口中声明待提炼类的共同操作;
让相关的class实现上述接口;
调整客户的型别声明,使得以运用该接口。

(4)Duplicate Observed Data:如果你的Large Class是一个GUI class,你可能需要把数据和行为移动到一个独立的领域对象去。
将Domain Data拷贝到一个domain object中,建立一个Observer模式,用以对Domain object和GUI object内的数据进行同步控制。
一个分层良好的系统,应该将处理用户界面和处理业务逻辑的代码分开。之所以这样做,原因有以下几点:
(1)你可能需要使用数个不同的用户界面来表现相同的业务逻辑:如果同时承担两种责任,用户界面会变得过分复杂;
(2)与GUI隔离之后,domain object的维护和演化都会更容易:你甚至可以让不同的开发者负责不同部分的开发。
作法:
修改presentation class,使其成为domain class的观察者。
针对GUI class内的domain data,使用Self Encapsulate Field。
编译,测试
在事件处理函数中加上对设值函数的调用,以“直接访问方式”更新GUI组件。
编译、测试。
在domain class中定义数据及其相关的访问函数。
修改presentation class中的访问函数,将它们的操作对象改为domain object。
 修改observer的update,使其从相应的domain object中所需的数据拷贝给GUI组件。
编译,测试。

 

看上去很像.net2005中的DataObject。在前台绑定GridView之后,可以将GridView中的一些行为(添加、修改、删除)与DataObject进行映射。比如:
<asp:ObjectDataSource ID="ObjectDataSource2" runat="server" SelectMethod="GetUsersInRole"
    TypeName
="System.Web.Security.Roles" OldValuesParameterFormatString="original_{0}" OnSelecting="ObjectDataSource2_Selecting">
    
<SelectParameters>
        
<asp:ControlParameter ControlID="GridView1" Name="roleName" PropertyName="SelectedValue"
            Type
="String" />
    
</SelectParameters>
</asp:ObjectDataSource>
代码:
[DataObject]
    
public static class RoleDB
    {
        [DataObjectMethod(DataObjectMethodType.Select)]
        
public static RoleInfoCollection GetAllRoles()
        {
            RoleInfoCollection infos 
= new RoleInfoCollection();
            
string[] roleNames = Roles.GetAllRoles();
            
foreach (string roleName in roleNames)
            {
                RoleInfo info 
= new RoleInfo();
                info.Name 
= roleName;
                info.Symbol 
= roleName;
                infos.Add(info);
            }
            
return infos;
        }
posted on 2007-07-01 00:10 littlegai 阅读(416) 评论(0)  编辑 收藏 引用

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   博问   Chat2DB   管理