重构常用手法(一)
重构:使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
重构的最终为了使项目达到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.pngclass 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判断数据的可用性。
总结:
- 业务和界面的分离应当在组件设计初始的时候考虑在内,同时在新增组件时贯彻分离的策略,保持代码的整洁度。
- 能够被重复使用的界面应当尽可能抽出公共部分,设计时尽量使用无状态的界面组件以精简逻辑。
- 业务与界面分离可能造成的后果是业务分离出来之后往往需要很多中间变量,并且可能经过多级传递才能够递交给界面组件,相比于分离的优势,这样的代价是可以接受的。
网友评论