当前位置:   article > 正文

[架构之路-184]-《软考-系统分析师》-15-系统运行与维护 - 逆向工程、再工程与软件重构_3,简述软件逆向工程的必要性和软件再工程的过程。

3,简述软件逆向工程的必要性和软件再工程的过程。

目录

一、常见概念

1. 逆 向 工 程( Reverse Engineering )  =》 自底向上还原的过程

2. 设计恢复 (design recovery )。

3. 正向工程 (Forward Engineering )。

4. 再工程( re - engineering )     =》 重构、重新实现

5.  重 构 ( restructuring ) =》换一种方式表述和实现

6. 完 备 性

二、什么软件重构

三、为什么要重构

四、重构的时机

五、重构的原则

六、代码重构示例

1,提炼函数

2,以查询取代临时变量

3.  引入解释性变量

4,分解临时变量

5,移除对参数的赋值

6,以函数对象取代函数

7,替换算法

8,内联函数


一、常见概念

1. 逆 向 工 程( Reverse Engineering )  =》 自底向上还原的过程

自底向上的还原过程称为逆向工程。!!!

自底向上的拼接过程称为系统集成。!!!

与逆向工程相关的概念有重构、设计恢复、再工程和正向工程。

 逆向工程术语源于硬件制造业,相互竞争的公司为了了解对方设计和制造工艺的机密,在得不到设计说明制造说明书的情况下,通过拆卸实物获得信息。

软件的逆向工程也基本类似,不过,通 常 “解剖”的不仅是竞争对手的程序,而且还包括本公司多年前的产品。

软件的逆向工程是分析程序,力图在比源代码更高抽象层次上建立程序的表示过程,逆向工程是设计恢复过程

2. 设计恢复 (design recovery )。

设计恢复是指借助工具从已有程序中抽象出有关数据设计、总体结构设计和过程设计等方面的信息。

3. 正向工程 (Forward Engineering )。

正向工程是指不仅从现有系统中恢复设计信息,而且使用该信息去改变或重构现有系统,以改善其整体质量。

4. 再工程( re - engineering )     =》 重构、重新实现

再工程是指在逆向工程获得信息的基础上,修改或重构已有的系统,产生系统的一个新版本

再工程是对现有系统的重新开发过程,包括先是逆向工程、然后是新需求的考虑过程最后正向工程三个步骤。

它不仅能从已存在的程序中重新获得设计信息,而且还能使用这些信息来重构现有系统,以改进它的综合质量

在利用再工程重构现有系统的同时,一般会增加新的需求,包括增加新的功能和改善系统的性能。
 

5.  重 构 ( restructuring ) =》换一种方式表述和实现

重构是指在同一抽象级别转换系统描述形式

重构强调的是同级别优化、替换、重新表达 !!!。

抽象级别有:

(1)软件重构(代码+数据)=》代码级 =》规范化、可读化、

  • 函数级
  • 模块级
  • 组件级
  • 程序级

(2)逻辑设计级 =》 模块级 =》 可复用、可移植

(3)功能级 =》架构级        =》 模块化、解耦、可扩展

(4)业务级 =》 业务级

重构强调的是同级别优化、替换、重新表达 !!!重构不一定需要先逆向、再正向!!!

如果重构前,已经有了现有代码的某一层次上的抽象,就不需要先进行逆向工程。

即使没有原先的某一层次的抽象,如果即接口,也是可以直接重构的。

当然,在重构前,没有现有系统的某一层次的抽象,也可以先进行逆向工程,理解现有系统,再进行重构(再工程)

6. 完 备 性

一般认为,凡是在软件生命周期内将软件某种形式的描述转换成更为抽象形式的活动都可称为逆向 工程。

逆向工程的完备性可以用在某一个抽象层次上提供信息的详细程度来描述。

逆向工程过程应该能够导出过程的设计模型(实现级,一种底层的抽象)、程序和数据结构信息(结构级,稍高层次的抽象)、对象模型、数据和控制流模型(功能级,相对高层的抽象)和 U M L 状态图和部署图(领域级,高层抽象)。

随着抽象层次增加,完备性就会降低。抽象层次越高,它与代码的距离就越远,通过逆向工程恢复的难度就越大,而自动工具支持的可能性相对变小,要求人参与判断和推理的工作增多。

逆向工程不仅应用于软件开发,也应用于软件维护。对于一项具体的维护任务,一般不必导出所有抽象级别上的信息,例如,如果只是希望完成代码重构任务,则只需获得实现级信息即可。当然,若能进行深入分析,产生的代码质量会更好些。

备注:

源代码本事是最完备、最齐全的,越是高层的抽象,信息越容易丢失!!!

二、什么软件重构

软件重构(Refactoring)就是通过调整程序代码改善软件的质量、性能,使其程序的代码实现设计模式架构更趋合理,提高软件的扩展性和维护性、方便提升性能。在即在不改变软件可观察行为的前提下,通过调整程序代码改善软件的质量、性能,使其程序的设计模式和架构更趋合理,提高软件的扩展性和维护性。

也即,在不改变功能特性的情况下,改善其非功能性特性

软件重构是指在不改变软件的功能外部可见性的情况下,为了改善软件的结构,提高软件的清晰性可扩展性可重用性而对其进行的改造。简而言之,重构就是改进已经写好的软件的设计。

软件重构需要借助工具完成,重构工具能够修改代码同时修改所有引用该代码的地方。

重构是代码维护中的一部分,既不修正错误,又不增加新的功能性。而是用于提高代码的可读性或者改变代码的结构和设计,使其在将来更容易被维护。特别是,在现有的程序的结构下,给一个程序增加一个新的行为会非常困难,因此开发人员可能先重构这部分代码,使加入新的行为变得容易。

这个术语是从数字与多项式的因式分解类比而来。如,x2 − 1 可以被分解为 (x + 1)(x − 1), 这样揭示了前面的形式不可见的内部结构(如两个根+1和-1)。同样,在软件重构中,在可见,结构上的改变通常会揭示原有代码中“隐藏”起来的内部结构

为了简化测试,重构是分步骤完成的。当重构结束后,任何外部行为上的变化无疑都是错误并可以与调试一个一个新特性的问题分开解决。

重构的好处 (非功能性需求)

  1. 重构可以改进程序内部结构,跟着技术不断发展,防止逐渐腐败
  2. 重构可以使软件产品的架构更清晰,简化代码裸机,去除重复代码,使人更容易理解,更容易维护,更容易持续演进。
  3. 重构可以使软件产品更容易拓展,提高开发效率。符合program smart标准:
  • performance高效
  • protable可移植
  • security安全
  • maintainable可维护
  • reliability可靠
  • readable可读
  • testable可测试

三、为什么要重构

关于为什么要重构,查阅相关资料或翻阅相关书籍大多都会提到以下几句话:

1、持续偏纠和改进软件设计 。=》 低阶

2、使代码更被其他人所理解。  =》 低阶

3、帮助发现隐藏的代码缺陷 =》 中阶

4、从长远来看,有助于提高编程效率,增加项目进度。 =》 中阶

5、有利于性能改进                   =》高阶

为了持续改进我们的软件设计使得机器跑我们的软件更轻松,为了让同事更容易理解我们写的代码,为了出更少的Bug,为了更快的开发!

其他原因:

  • 编码问题:大量重复代码让人很难理解,新增特性需要涉及阅读理解其他一大堆的无用代码。
  • 编码问题:模块内的功能实现堆砌式,随便使用全局变量、控制变量,不断增加控制分支,导致维护难度不断增加,程序异常也不断增加
  • 架构问题:程序接口文档缺失,模块内外耦合严重
  • 性能问题:运行时逐渐出现内存、CPU、存储空间等资源不足,使用过高,等性能问题,急切优化程序
  • 软件工程:功能类似,代码重复,工作量增加,效率很低,不利于软件发展等等。

四、重构的时机

(1)重构时机

重构应该是随时进行,不要为了重构而重构,重构是为了将我们的程序优化得更好,更更好的为我们省事,高效。所以我们需要把握好重构时机,及时重构。

  • 重复的事情,果断重构:如果做一件事情,重复超过三次,那么第三次就得选择重构去做。
  • 新增特性代码的时候:发现有更好的实现,方便未来拓展,使开发更快速、更舒畅,可以考虑重构。
  • 修改bug的时候:发现原来的旧代码不够清晰,并且很难发现错误,可以考虑重构
  • 在我们组织代码检视活动的时候:根据经验,存在问题,有改善设计的空间,可以考虑重构。
  • 如果代码太混乱,设计完全错误,不考虑重构,直接重写,重新设计。

(2)不重构情形

  • 如果没有任何重构思路的时候,不要考虑重构。
  • 如果时间很紧,重构的工作量又很大,会影响整个工作,建议推迟重构。
  • 不要为了重构而重构

五、重构的原则

  1. 重构的前提是:先识别软件代码的坏味道,制定好重构方案后,才能进行重构。
  2. 重构时最重要的一点是:需要将新增代码与重构分开,重构前与重构后需要有测试用例保障,确保继承性功能不被破坏。重构后,进行回归测试,因此,不要增加新功能。新增新功能或者修改bug时,不要将重构代码放在一起带进去,新增新功能只管添加新功能代码即可。重构时,也不能带有新功能,只管改进原来的程序结构即可
  3. 一次只做一件事,一次重构只针对一种类型功能进行重构。

六、代码重构示例

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++编译器会调用内联函数的地方展开,没有函数压栈开销,内联函数提升程序运行的效率。适合针对与性能敏感的函数逻辑。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/90680
推荐阅读
相关标签
  

闽ICP备14008679号