前言: 我们做程序员这一行的哈,说白了就是用机器能识别的语言跟机器打交道,但是经常会出现的一种情况是,我们所写的代码只有自己和机器能看得懂,其他人看懂有一定的困难性,甚至有时候过了一段时间连自己都看不懂了。。。那么此时你应该感叹自己当时的水平高超,还是该好好的反思一下了?

一,重构是什么    
    OK,闲言碎语不多讲,啥叫重构呢?随手打开百度,输入“重构”,点开百度百科:重构(Refactoring)就是通过调整程序代码改善软件的质量、性能,使其程序的设计模式和架构更趋合理,提高软件的扩展性和维护性。但是这个解释缺少了一个前提,完整的说法应该是:在不改变软件可观察行为的前提下,通过调整程序代码改善软件的质量、性能,使其程序的设计模式和架构更趋合理,提高软件的扩展性和维护性。

当我第一次读到这句话时,感觉重构这件事情跟我这个满头秀发的初级程序员没有任何关系,还什么设计模式,还什么架构,好高深的东西。。但是随着发型趋于成熟和结合踩过的越来越多的坑,逐渐改变了这个想法。

我这里不是某个不合格的老师上课只会读PPT,当然我也没有资格去做老师。我所相信的是,任何东西,任何学科都能用最简单的普世理论讲的通,也都能在自然界或人类社会中找到某个大家所熟知的例子进行类比,尤其是自然学科。说白了,不管是给一个普通人还是行业顶尖人解释一个复杂的功能或概念,用生活中常见的例子加上人的惯性思维比照本宣科更容易让人理解!咱是个粗人,怎么理解重构这个概念呢?前提:不能改变软件原来有的功能,我做了点什么事呢:改了改代码,然后呢:机器跑这个软件轻松了或其他同事更容易理解这段代码了(此处立个Flag,或许下次再读这句话时会重构它)。

举个例子,当你穿的衣衫不整的就去参加亲戚或朋友的婚礼了,而且表情愤怒,言语不羁,认识你的人知道你是来参加婚礼,不知道的以为你是新娘的前男友去砸场子呢,容易被门口保安小哥按住。如何避免这种尴尬呢?很简单,重构一下啊:把头发梳成大人模样,穿上一身帅气西装,手里拿着红包,然后满脸微笑,见了人多打招呼。这样的话别人一看就知道你是来干啥滴。你还是你,该做的事情做得更好了,保安小哥也不会拦你了,说不定门口的礼仪小姐姐还会和你打招呼,而且逼格也提升了好几个level。设身处地的想一想,我们的仪表着装言谈举止不就是我们个人社交时的“对外接口”么。

OK,回到软件代码的重构,结合刚才那个例子,其实把一个很low函数名改成一个容易让人理解的名字,也属于重构。这样不管是自己还是其他人一看函数名字就大体知道这个函数是要干啥滴!

二,为什么要重构
      关于为什么要重构,查阅相关资料或翻阅相关书籍大多都会提到以下几句话:1、持续偏纠和改进软件设计 。2、使代码更被其他人所理解。 3、帮助发现隐藏的代码缺陷 。4、从长远来看,有助于提高编程效率,增加项目进度。

其实将什么是重构时已稍微带入了,再次强调一下,为了持续改进我们的软件设计使得机器跑我们的软件更轻松,为了让同事更容易理解我们写的代码,为了出更少的Bug,为了更快的开发! 简单明了而且就连百度百科上也解释的很清楚,此处不多赘述。

三,怎么重构
        哲学上有世界观和方法论,同样此时我们已经对重构有了一定的了解,那么我们通过什么方法进行重构呢?

OK,接下来要分不同的情况进行讨论了。

1,提炼函数

具体做法:①拆分过长的函数。②提炼在多个函数中使用的相同代码

这样做的好处是:①函数被复用的机会大。②如果所提炼的函数命名足够好,那么高层函数的功能更加一目了然

比如以下函数(代码是C/C++,但是 C#, Java的同学都可以看得懂):

int nArrays[5] = {1,2,3,2,1};
void PrintResult()
{
    int nPrintNum ;
 
    // Print split line;
    printf("****************************");
    printf("********             *******");
    printf("****************************");
 
    // Algorithm;
    for (int i = 0;i<5;i++)
    {
        nPrintNum += nArrays[i] * 5;
    }
    // Result:
    printf("Result: %d" , nPrintNum);     
}

其实这个函数已经很简单了,我这里举一个简单的例子简单说明一下(代码纯手敲,并没有做过运行)

分析一下哈,首先这个函数名是叫"PrintResult",个人感觉里面最好不要包含算法,所以将算法应该提取出来,当然上面Pring splist line也可以提取出来:

void PrintSplitLine()
{
    printf("****************************");
    printf("********             *******");
    printf("****************************");
}
 
int CalcResult()
{
    int nPrintNum = 0;
    for (int i = 0;i<5;i++)
    {
        nPrintNum += nArrays[i] * 5;
    }
    return nPrintNum;
}
 
void PrintResult()
{
    PrintSplitLine();
    int Result = CalcResult();
    printf("Result: %d" , Result );  
}

这样的话PrintResult()这个函数甚至连注释都不用加大家就都能看懂,而且如果其他地方需要打印分割线或者使用这个算法时候,这两个函数都可以复用了。

2,以查询取代临时变量

比如上一段代码定义了一个为Result申请了一个临时变量,当然也无可厚非,提倡的做法是直接查询就可以了:

void PrintResult()
{
    PrintSplitLine();
    printf("Result: %d" , CalcResult());  
}
    3.引入解释性变量

大家看下下面的代码,虽然加了注释,但是看起来依然不爽,揍啥嘞界是!

double GetPrice()
{
    //price is base price - quantity discount + shipping
    return m_nQuantity * m_nItemPrice - max(0,m_nQuantity-500) * n_ItemPrice * 0.05 + min(m_nQuantity * m_ItemPrice * 01, 100.0 );
}
        这个表达式太复杂,我们应该将表达式拆分,拆分的结果应该放到临时变量里,以变量名称来解释表达式的用途:

double GetPrice()
{
    double dBasePrice = m_nQuantity * m_nItemPrice;
    double dQuantityDiscount = max(0, m_nQuantity - 500) *  m_nItemPrice * 0.05;
    double dShipping = min( dBasePrice * 0.1, 100.0);
    return dBasePrice - dQuantityDiscount + dShipping;
}
    4,分解临时变量

临时变量通常用于保存临时运算结果,这种临时变量应该只被赋值一次。如果它被赋值超过一次意味着它承担了一个以上的责任,当然这也无可厚非,但是这使得其他人会产生疑惑,这个值到底是干嘛用的?

比如下面这个函数,本来就已经够复杂的了,但是大家会发现dAcc赋值了两次,而这两次是属于不同的加速度,都用这个变量表示容易让人产生误解。

double GetDistanceTravelled(int nTime)
{
    double nResult = 0;
    // 加速度
    double dAcc = m_dPrimaryForce / m_dMass;
    int nPrimaryTime = min(nTime, m_dDelay);
    nResult = 0.5 * dAcc * nPrimaryTime * nPrimaryTime;
    int nSecondaryTime = nTime - m_dDelay;
    if (nSecondaryTime > 0)
    {
        double nPrimaryVel = dAcc * m_dDelay;
        dAcc = (m_dPrimaryForce + m_dSecondaryForce) / m_dMass;
        nResult += nPrimaryVel * nSecondaryTime + 0.5 * dAcc * nSecondaryTime * nSecondaryTime;
    }
    return nResult;
}

不同含义的变量应该用两个不同的临时变量:

double GetDistanceTravelled(int nTime)
{
    double nResult = 0;
    // 加速度
    const double dPrimaryAcc = m_dPrimaryForce / m_dMass;
    int nPrimaryTime = min(nTime, m_dDelay);
    nResult = 0.5 * dAcc * nPrimaryTime * nPrimaryTime;
    int nSecondaryTime = nTime - m_dDelay;
    if (nSecondaryTime > 0)
    {
        double nPrimaryVel = dPrimaryAcc * m_dDelay;
        const double dSecondaryAcc = (m_dPrimaryForce + m_dSecondaryForce) / m_dMass;
        nResult += nPrimaryVel * nSecondaryTime + 0.5 * dSecondaryAcc * nSecondaryTime * nSecondaryTime;
    }
    return nResult;
}

5,移除对参数的赋值

首先观察下面的函数的某个片段:

void Method( Object obj) 
{
    int nResult = obj.GetValue();       // 1
    Object anotherObject = new Object();// 2
    obj = anotherObject;                // 3
    // ...

      解释一下,首先obj是一个参数,在函数内改变它的值是一个不好的习惯,其次obj是当做一个对象传进来的,也没办法改变它的值。如果在Method函数里面只是想获取某些obj里面的值,下面这种写法是提倡的:

void Method( const Object& obj) 
{
    int nResult = obj.GetValue(); 
    Object anotherObject = new Object();
    // ...

        首先我不改变传入的值所以加了个const,其次传引用只需要传递四个字节。这里需要注意的是Object类中的GetValue()应该也加上const:GetValue() const{ ... },因为const对象不能引用非const成员函数。另外函数加上了const,在函数内就不能改变成员变量了。

再举个简单的例子:

int Discount (int nInputVal, int nQuantity, int nYearToData) 
{
    if (nInputVal > 50) nInputVal -= 2;
    if (nQuantity > 100) nInputVal -= 1;
    if (nYearToData > 10000) nInputVal -= 4;
    return nInputVal;    
}
        这种直接这种传入值,转了一圈又当做返回值出去了。。。不要修改传入值,否则容易让人混淆。提倡做法:

int Discount (int nInputVal, int nQuantity, int nYearToData) 
{
    int nResult = nInputVal;
    if (nInputVal > 50) nResult -= 2;
    if (nQuantity > 100) nResult -= 1;
    if (nYearToData > 10000) nResult -= 4;
    return nResult;    
}
    6,以函数对象取代函数

如果有一个大型函数,局部变量过多,可以将这个函数放到一个单独的对象中,如此一来,局部变量就变成了对象内的字段。然后可以在同一个对象中将这个大型的函数分解成多个小函数。另外,局部变量过多时,首先考虑“以查询取代临时变量”,有时候你会发现根本无法拆解一个需要拆解的函数,在这种情况下考虑使用函数对象。

class Order
{
    // ...
        double Price()
        {
            double primaryBasePrice;
            double secondaryBasePrice;
            double tertiaryBasePrice;
            //long computation;
            // ...
        }
}
        可参考以下类图:

7,替换算法

把某个算法替换为更清晰的算法:

下面这段代码是Java的代码,懂得C++或C#的童鞋应该能直接看懂。两段代码要实现的功能一致,至于你认为哪一种好用,就用哪一种呗(个人比较推崇第二段实现方式)。

String foundPerson( String[] people ){
    for ( int i=0; i < people.length; i++ )  {    
        if ( people[i].equals("Don") ) {
            return "Don";
        }        
        if ( people[i].equals("John") ) {
            return "John";
        }
        if ( people[i].equals("Kent") ) {
             return "Kent";
        }
    }
    return "";
}
String foundPerson( String[] people ){
    List candidates = Arrays.asList(new String[]{"Don","John","Kent"});
    for ( int i=0; i < people.length; i++ )    {
        if ( candidates.contains(people[i) ) {
            return people[i];
        }        
    }
    return "";
}
8,内联函数

在C++中,以inline修饰的函数叫做内联函数,编译时C++编译器会调用内联函数的地方展开,没有函数压栈开销,内联函数提升程序运行的效率。

内联函数用法大家自行百度就可以了,此处不再啰嗦。

OK,暂时先给大家分享到这里。欢迎批评指正。
————————————————
版权声明:本文为CSDN博主「Jon__Wang」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_36282233/article/details/84892188

软件设计中,何为重构?相关推荐

  1. 软件设计中的一些原则

    本文为大家介绍软件设计中的一些原则,都是经过长期经验总结出来的知识,每一个程序员都应该了解,相信对大家在进行软件设计的过程中会有很大帮助. Don't Repeat Yourself (DRY) DR ...

  2. 第九十期:哪种人是软件设计中的稀缺型人才?

    好的系统架构离不开好的接口设计,因此,真正懂接口设计的人往往是软件设计队伍中的稀缺型人才. 作者:从码农到工匠 好的系统架构离不开好的接口设计,因此,真正懂接口设计的人往往是软件设计队伍中的稀缺型人才 ...

  3. 哪种人是软件设计中的稀缺型人才?

    阿里妹导读:好的系统架构离不开好的接口设计,因此,真正懂接口设计的人往往是软件设计队伍中的稀缺型人才. 为什么在接口制定标准中说:一流的企业做标准,二流的企业做品牌,三流的企业做产品?依赖倒置到底是什 ...

  4. 漫谈 · 软件设计中的具象化

    本文微信公众号链接:https://mp.weixin.qq.com/s/PiZU1biNR5DeqrjnhXE9ag 何为具象化?要说具象,就要说说与具象有关的抽象.表象. 抽象与具象: 抽象是通过 ...

  5. 从奥运门票系统瘫痪到家乐福踩踏事件看软件设计中业务模型的处理

    从奥运门票系统瘫痪到家乐福踩踏事件看软件设计中业务模型的处理 作者:郭方明 完成日期:2007-11-17 version 1.0 联系信箱:gfm.job@Gmail.com 注:转载文章,请注明作 ...

  6. 【转载】软件设计中的易用性

    软件设计中的易用性 摘要: 这篇文章介绍了软件设计中"易用性"的概念并解释了为什么它在软件设计项目中应该是一个重要的部分. 介绍 应用"易用性"到软件开发中 & ...

  7. 软件设计中的抽象层次

    The programmers of old were mysterious and profound. We cannot fathom their thoughts, so all we do i ...

  8. 软件设计中的“自上而下”和“自下而上”

    在切入主题之前先要了解"上"与"下"的含意是什么,这需要从图1中找答案.图中,应用层在最上面,其下依次是框架.平台.库和操作系统层,因此"上" ...

  9. 软件设计中的csc_通用集中监控中心CSC解决方案

    概述:中兴力维推出的通用集中监控中心CSC解决方案,建设动环监控系统的省级监控中心,对各个地市的区域监控中心LSC集中监控,从而实现对全省监控系统的统一管理,实现全省电源设备资源的共享,提升各运营商全 ...

最新文章

  1. R配对卡方检验(McNemar‘s Test)
  2. Kong APIGW — 安装与配置
  3. python清除缓存的命令_python – 重启django服务器时清除缓存的最佳位置
  4. 详解联邦学习Federated Learning
  5. Winsock网络编程快速入门
  6. 感知器算法的基本原理和步骤_很多情况下,深度学习算法和人脑相似
  7. 必知!4张图看尽AI发展史重大里程碑
  8. mac mysql sequel_苹果系统Sequel Pro—MySQL客户端工具一个大坑
  9. 类HTML语法显示格式化文本
  10. angular 个人零点学习
  11. 职业教育相关的核心期刊有哪些?
  12. matlab如何整理表格数据,数据整理的程序与步骤:包括数据预处理、分类或分组、图表显示...
  13. 择一城终老,遇一人白首
  14. 移动硬盘无法退出终极解决方法
  15. 计算机中的网络协议包括哪些,网络协议三要素有什么关系
  16. 36 个助你成为专家需要掌握的 JavaScript 概念
  17. 学习teardrop攻击并伪造一个ip包
  18. 开关电源计算机仿真技术pdf,《开关电源仿真设计》PPT课件.ppt
  19. 华为matebook13进入Bios,重装系统,切换启动顺序,选择U盘启动
  20. js月份的计算公式_JS根据生日月份和日期计算星座的简单实现方法

热门文章

  1. 求一份fm收音机源码
  2. 搜狗两大AI翻译神器亮相CES 实力演绎用中文与世界对话
  3. 联易融港交所上市:金融、科技一把抓,研发投入不及明源云等
  4. 【Python游戏】用Python 和 Pyglet 编写一个我的世界小游戏 | 附源码
  5. 起底B站、西瓜抢人之争
  6. tqdm介绍及常用方法
  7. Android Audio播放音频之数据传递
  8. 直接映射缓存,全相联映射缓存,组相连映射与tag,index,offset的理解
  9. 树莓派c语言按键开关,树莓派 GPIO按钮开关 原理与实现
  10. ASP.NET MVC 实现页落网资源分享网站+充值管理+后台管理(1)之数据库设计