2018-08-26

重构常用手法(一)

重构:使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。

重构的最终为了使项目达到clean code,不管项目在开发中还是维护中都有可能需要对项目进行重构,本文列举了项目中常见的重构的坏味道和常见的重构手法。�

项目中常见的问题:

  • 重复的代码
  • 过长的函数
  • 过大的类
  • 过长的参数列表
  • 发散式变化
  • 过多的注释

重构常用的方法:

1. 提炼函数

代码都是以函数为单位,提炼函数使项目中常用的手法。提炼函数的常用的几个场景:

  • 函数过长
  • 一个函数实现多个功能
  • 代码重复
  • 当一段代码需要添加注释
    下段代码中可以看到一个函数可以明显分为三部分,实现了三个功能,需要添加注释说明意图,此时可单独进行提炼。
void printOwing() {
    Enumeration e = _orders.elements(); 
    double outstanding = 0.0; 
    // print banner
    System.out.println ("**************************"); 
    System.out.println ("***** Customer Owes ******"); 
    System.out.println ("**************************"); 
    // calculate outstanding 
    while (e.hasMoreElements()) { 
            Order each = (Order) e.nextElement(); 
            outstanding += each.getAmount(); 
    } 
   //print details 
   System.out.println ("name:" + _name); 
   System.out.println ("amount" + outstanding); 
}

=>

void printOwing(double previousAmount) { 
      printBanner(); 
      double outstanding = getOutstanding(previousAmount * 1.2);               
      printDetails(outstanding); 
}

void printBanner() { 
    System.out.println ("**************************"); 
    System.out.println ("***** Customer Owes ******"); 
    System.out.println ("**************************"); 
} 
double getOutstanding(double initialValue) { 
    double result = initialValue; 
    Enumeration e = _orders.elements(); 
    while (e.hasMoreElements()) { 
        Order each = (Order) e.nextElement(); 
        result += each.getAmount(); 
    } 
    return result;
}

void printDetails (double outstanding) { 
    System.out.println ("name:" + _name); 
    System.out.println ("amount" + outstanding); 
}

在表达式复杂让人难以理解时,可以提取函数,通过函数名来解释表达式的用途。

double price() { 
    // price is base price - quantity discount + shipping 
    return _quantity * _itemPrice - Math.max(0, _quantity - 500) * _itemPrice * 0.05 + Math.min(_quantity * _itemPrice * 0.1, 100.0); 
}

=>

double price() { 
    return basePrice() - quantityDiscount() + shipping(); 
}
private double quantityDiscount() { 
    return Math.max(0, _quantity - 500) * _itemPrice * 0.05; 
} 
private double shipping() { 
    return Math.min(basePrice() * 0.1, 100.0); 
} 
private double basePrice() {
    return _quantity * _itemPrice; 
}

总结:

  • 一个函数最好只实现一个功能,函数简短情况下更好的达到复用的结果
  • 函数过长的情况下应该进行函数的提取,衡量一个函数长短是否合适,在于函数名是否能清晰函数体的意图。
  • 函数提取不只是在函数过长情况下才可以进行提取,在函数需要解释的时候,可以提取函数通过函数名进行解释,因此一个函数可以只有一行代码
  • 如果代码段中临时变量过多,应该使用其他手法(查询替换临时变量)先对临时变量进行替换处理

2. 卫语句

在复杂业务场景下,一个函数难免会出现复杂的条件逻辑判断,让人难以理解函数的执行路径,应该使用卫语句来表现所及特殊情况。

double getPayAmount() { 
    double result; 
    if (_isDead) 
        result = deadAmount(); 
    else { 
        if (_isSeparated) 
            result = separatedAmount(); 
        else { 
            if (_isRetired) 
                result = retiredAmount(); 
            else result = normalPayAmount(); 
        }; 
    } 
    return result; 
};

=>

double getPayAmount() { 
    if (_isDead) 
        return deadAmount(); 
   if (_isSeparated) 
        return separatedAmount(); 
    if (_isRetired) 
        return retiredAmount(); 
    return normalPayAmount(); 
};

为使用卫语句,可以将条件表达式逆反,

public double getAdjustedCapital() { 
    double result = 0.0; 
    if (_capital > 0.0) { 
        if (_intRate > 0.0 && _duration > 0.0) { 
            result = (_income / _duration) * ADJ_FACTOR; 
        } 
    } 
    return result; 
}

=>

public double getAdjustedCapital() { 
    double result = 0.0; 
    if (_capital <= 0.0) 
        return result; 
    if (_intRate <= 0.0 || _duration <= 0.0) 
        return result; 
    return (_income / _duration) * ADJ_FACTOR; 
}

总结:

  • 卫语句可以降低逻辑的嵌套,明确执行路径
  • 为使用卫语句,有时候需要将条件表达式进行重新组织(调整顺序、逆反)
  • 简单的if/else 语句也可以使用卫语句

3. 引入解释性变量:

在表达式复杂情况下,可将表达式进行分解、各部分替换为临时变量,以此变量名称来解释表达式用途。

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 
}

总结:

  • 在较长算法中,需要保存中间结果,推荐运用临时变量来解释每一步运算的意义
  • 也可以使用提炼函数来解释表达式的意义。

4. 提取类:

一个类的设计应该符合单一指责原则。一个类如果过于复杂,做了好多的事情,违背了“单一职责”的原则,所以需要将其可以独立的模块进行拆分,当然有可能由一个类拆分出多个类。

Extract Class - Before.png
class Person... 
    public String getName() { 
        return _name; 
    } 
    public String getTelephoneNumber() { 
        return ("(" + _officeAreaCode + ") " + _officeNumber); 
     } 
    String getOfficeAreaCode() { 
        return _officeAreaCode; 
    } 
    void setOfficeAreaCode(String arg) { 
        _officeAreaCode = arg; 
    } 
    String getOfficeNumber() { 
        return _officeNumber; 
    } 
    void setOfficeNumber(String arg) { 
        _officeNumber = arg; 
    } 
    private String _name; 
    private String _officeAreaCode; 
    private String _officeNumber;
Extract Class - After.png
class Person... 
    public String getName() { 
        return _name; 
    } 
    public String getTelephoneNumber(){ 
        return _officeTelephone.getTelephoneNumber(); 
    } 
    TelephoneNumber getOfficeTelephone() { 
        return _officeTelephone; 
    } 
    private String _name; 
    private TelephoneNumber _officeTelephone = new TelephoneNumber(); 

class TelephoneNumber... 
    public String getTelephoneNumber() { 
         return ("(" + _areaCode + ") " + _number); 
     } 
    String getAreaCode() { 
        return _areaCode; 
     } 
    void setAreaCode(String arg) {
         _areaCode = arg; 
    } 
    String getNumber() { 
        return _number; 
    } 
    void setNumber(String arg) { 
        _number = arg; 
     } 
    private String _number; 
    private String _areaCode;

总结:

  • 对类的细化减少代码的重复性,以及提高代码的复用性,便于代码的维护
  • 提取的新类应该有确认的职责,但如果没有承担足够的职责,则应该对类进行内联化

5. 业务与显示分离

GUI类中,用户界面显示代码和业务逻辑代码应该进行分离。用户界面代码不包含任何业务数据,可以达到很好的复用;业务逻辑代码不包含界面显示代码,使得同一业务逻辑代码的多种展现方式成为可能。
以如下一个展示评论列表的控件为例:

class CommentList extends React.Component {
  constructor() {
    super();
    this.state = { comments: [] }
  }
  componentDidMount() {
    $.ajax({
      url: "/my-comments.json",
      dataType: 'json',
      success: function(comments) {
        this.setState({comments: comments});
      }.bind(this)
    });
  }
  render() {
    return <ul> {this.state.comments.map(renderComment)} </ul>;
  }
  renderComment({body, author}) {
    return <li>{body}—{author}</li>;
  }
}

在该控件中数据在componentDidMount函数中通过ajax请求获取,界面显示在render和renderComment中完成,因此该组件具备较高的定制性,更换位置和变量之后需要重写一份相似的函数,难以重用。
同时组件对于comments数据也没有专门的数据检查,难以达到复用的效果。为了达到代码复用和数据检查的效果,将业务逻辑与显示分离,可以得到如下的代码:

class CommentListContainer extends React.Component {
  constructor() {
    super();
    this.state = { comments: [] }
  }
  componentDidMount() {
    $.ajax({
      url: "/my-comments.json",
      dataType: 'json',
      success: function(comments) {
        this.setState({comments: comments});
      }.bind(this)
    });
  }
  render() {
    return <CommentList comments={this.state.comments} />;
  }
}
class CommentList extends React.Component {
  constructor(props) {
    super(props);
  }
  render() { 
    return <ul> {this.props.comments.map(renderComment)} </ul>;
  }
  renderComment({body, author}) {
    return <li>{body}—{author}</li>;
  }
}

这样就做到了数据提取和渲染分离,CommentList可以复用,同时CommentList可以设置PropTypes判断数据的可用性。

总结:

  • 业务和界面的分离应当在组件设计初始的时候考虑在内,同时在新增组件时贯彻分离的策略,保持代码的整洁度。
  • 能够被重复使用的界面应当尽可能抽出公共部分,设计时尽量使用无状态的界面组件以精简逻辑。
  • 业务与界面分离可能造成的后果是业务分离出来之后往往需要很多中间变量,并且可能经过多级传递才能够递交给界面组件,相比于分离的优势,这样的代价是可以接受的。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,752评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,100评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,244评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,099评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,210评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,307评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,346评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,133评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,546评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,849评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,019评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,702评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,331评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,030评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,260评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,871评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,898评论 2 351

推荐阅读更多精彩内容