`

【转载】软件开发-重构

阅读更多

 

【原文网址】软件开发-重构

【原文作者】 人月神话的BLOG

 

重构是对软件内部结构的一种调整,目的是在不改变软件之可察性前提下,提高其可理解性,降低其修改成本。关于重构的至理明言如下:

  • 任何一个傻瓜都能写出计算器可以理解的代码,唯有写出人类容易理解的代码,才是优秀的程序员;
  • 事不过三,三则重构;
  • 当你接获bug提报,请先撰写一个单元测试来揭发这个bug;
  • 当你感觉需要撰写注释,请先尝试重构,试着让所有的注释变得多余;
  • 当你发现自己需要为程序增加一个特性,而代码结构使你无法方便的这样做,就先重构那个程序;
  • 重构之前,必须建立一套可靠的测试机制;
  • 写软件就像种树,优秀的程序员挖成小坑后随及填好,继续挖下一个,只会产生一系列小坑,不会有大坑, 菜鸟则不会意识到所挖的坑正在变大,还是不停的挖,直到自己掉进大坑,爬不出来,陷入无尽的痛苦深渊;
  • 开发时间越长,越能体会垃圾代码的痛苦,却不知道如何改进;
  • Kent Beck:我不是一个伟大的程序员,我只是个有着一些优秀习惯的好程序员而已;


变量(Variable)


不要定义一个临时变量多次重复使用,临时变量定义仍然应该可以自解释,从变量名称能够很好的理解变量的含义和作用。在定义一个临时变量后需要有一段业务逻 辑才能够完成对临时变量的赋值的时候,可以考虑将这段逻辑抽取到一个独立的方法。


 

    double getPrice() {
 
            int basePrice = _quantity * _itemPrice;
 
            double discountFactor;
 
            if (basePrice > 1000) discountFactor = 0.95;
 
            else discountFactor = 0.98;
 
            return basePrice * discountFactor;
 
    }

重构为:


    double getPrice() {
        return basePrice() * discountFactor();
    }

    private int basePrice() {
        return _quantity * _itemPrice;
    }

    private double discountFactor() {
        if (basePrice() > 1000) return 0.95;
        else return 0.98;
    }

当遇到复杂的表达式的时候,需要引入解释变量,因为复杂的表达式很难进行自解释。


if ( (platform.toUpperCase().indexOf("MAC") > -1) &&

 
    (browser.toUpperCase().indexOf("IE") > -1) &&
 
      wasInitialized() && resize > 0 )
{
 
        // do something
}

重构为:


final boolean isMacOs     = platform.toUpperCase().indexOf("MAC") > -1;
final boolean isIEBrowser = browser.toUpperCase().indexOf("IE")  > -1;
final boolean wasResized  = resize > 0;

if (isMacOs && isIEBrowser && wasInitialized() && wasResized) {
    // do something
}

减少对全局变量的使用,第一个是全局变量的生命周期很难控制,资源本身无法得到很快的释放,其二是过多使用全局变量导致在调用方法的时候很难完全清楚方法 说需要的入口数据信息,其三,多处都可以对全局变量赋值,我们很难立刻定位到当前全局变量的值来源自哪里?


分解方法(Extract Method)


一个较大的方法往往也会分为多个小的段落,step1,step2,step3,在每一个步骤都会考虑添加注释说明。而这些相对较为独立的步骤就可以分解 为不同的方法,在分解后方法名可以自解释方法的功能而不再需要额外的注释。在一个类里面如果方法里面有一段代码在多个方法中重复出现,需要抽取该类的公用 方法。在多个不同的类中有一段代码重复出现,需要考虑将公用代码放到公用类中形成公用方法。


方法名需要很好的自解释方法的功能,方法的返回尽量单一,方法的入口参数太多的时候应该考虑使用集合,结构或数据对象进行参数的传递。参数的传递可能出传 递的是引用,但不要去修改入口参数的值。


不要因为一个方法里面只有一行,两行很短而不考虑去分解,分解的时候更多的是考虑代码的自解释性。代码本身不是解释的技术实现机制,而是解释的业务规则和 需求。如果代码不是解释的业务规则和需求,那么其它人员就很难快速理解。


引入方法对象来取代方法,当发现一个方法只用到该类里面的几个关键属性,方法和类里面其它的方法交互很少,输出单一。由于该方法和这几个属性内聚性很强而 和该类其它部分松耦合,因此可以考虑将方法和这部分属性移出形成一个单独的方法对象。


移动方法,类的职责要单一,一个类的方法更多用到了别的类的属性,这个方法可能更适合定义在那个类中。


class Account...

 
  private AccountType _type;
 
  private int _daysOverdrawn;
 
 
 
  double overdraftCharge() {
 
          if (_type.isPremium()) {
 
                  double result = 10;
 
                  if (_daysOverdrawn > 7) result += (_daysOverdrawn - 7) * 0.85;
 
                  return result;
 
          }
 
          else return _daysOverdrawn * 1.75;
 
  }

    double bankCharge() {
 
          double result = 4.5;
 
          if (_daysOverdrawn > 0) result += overdraftCharge();
 
          return result;
 
  }

重构为:


class Account...
   private AccountType _type;
   private int _daysOverdrawn;    
   double overdraftCharge() {
       return _type.overdraftCharge(_daysOverdrawn);
   }

   double bankCharge() {
       double result = 4.5;
       if (_daysOverdrawn > 0)
           result += _type.overdraftCharge(_daysOverdrawn);
       return result;
   }

class AccountType...
   double overdraftCharge(Account account) {
       if (isPremium()) {
           double result = 10;
           if (account.getDaysOverdrawn() > 7)
              result += (account.getDaysOverdrawn() - 7) * 0.85;
           return result;
       }
       else return account.getDaysOverdrawn() * 1.75;
   }

分解类(Extract Class)


类的职责的划分不容易在初次设计时就准确把握,所以在编码时重构是必要的。职责定位不清!——典型特征是拥有太多的成员变量;而在这里面最重要的就是职责 要单一,属性和方法是否合适的类中。如果不是就需要考虑分解或合并,扩展类的功能,或者抽象相应的接口。面向对象的设计原则如下:


1.单一职责原则 (SRP) - 就一个类而言,应该仅有一个引起它变化的原因


类的职责要单一,类里面的方法只做类的职责范围里面的事情。MVC即是一种粗粒度的职责话费,模型类重点是提供数据,控制类重点是处理业务逻辑,而V视图 类则是关注数据获取后的呈现。


数据和数据操作可以考虑分解,如形成专门的DTO数据传输对象类。界面类和界面数据提供类也可以考虑分离,如形成专门的Facade层专门负责数据的准备 和形成。界面层不应该有太多的数据处理操作。


当发现一个大的类里面的属性和方法存在明细的分组特性的时候,而且分组直接松散耦合,需要考虑分解为多个类。


引入方法对象来取代方法,当发现一个方法只用到该类里面的几个关键属性,方法和类里面其它的方法交互很少,输出单一。由于该方法和这几个属性内聚性很强而 和该类其它部分松耦合,因此可以考虑将方法和这部分属性移出形成一个单独的方法对象。


胖接口也是违反职责单一,胖接口会导致所有实现接口的类都Override所有的接口方法,而有些接口方法往往是子类并不需要的。因此对于胖接口仍然要从 职责的角度对接口进行拆分。


2.开放——封闭原则 (OCP)- 对扩展开放,对修改封闭


当发生变化时,只需要添加新的代码,而不必改动已经正常运行的代码:软件人的梦想!而要达到这个目的,关键是要能够较为准确的预测业务变化会导致的可能会 发送变化的模块或代码。


3.Liskov替换原则 (LSP)


子类型必须能够替换掉他们的基类型。正是子类型的可替换性,才使得使用基类类型的软件无须修改就可以扩展。案例参考正方形驳论。矩形的合理假设:长、宽可 以独立变化;而正方形的合理假设:长、宽始终相等。因此正方形并不能从矩形继承。


4.依赖倒置原则 (DIP) - 高层模块不应该依赖于低层模块;抽象不应该依赖于细节。

软件开发-重构


依赖倒置原则的重点是高层模块类不要去依赖底层模块的类,而应该去依赖接口,特别是当我们预见到底层模块的类本身可能会扩展和变化的时候。这样在变化的时 候最大的好处就是高层类和接口不用变化。


类是否考虑抽象为接口,一方面是根据LSP原则进行重构,一方面是需要观察我们建立的类,是否有多个类本身存在相同的行为或方法,如果存在则需要考虑抽象 接口。
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics