### Extract Method(提炼函数)
你有一段代码可以被组织在一起并独立出来。
将这段代码放进一个独立函数中,并让函数名称解释该函数的用途。
~~~
void printOwing(double amount) {
printBanner();
//print details
System.out.println ("name:" + _name);
System.out.println ("amount" + amount);
}
~~~
=>
~~~
void printOwing(double amount) {
printBanner();
printDetails(amount);
}
void printDetails (double amount) {
System.out.println ("name:" + _name);
System.out.println ("amount" + amount);
}
~~~
**动机(Motivation)**
Extract Method是我最常用的重构手法之一。当我看见一个过长的函数或者一段需要注释才能让人理解用途的代码,我就会将这段代码放进一个独立函数中。
有数个原因造成我喜欢简短而有良好命名的函数。首先,如果每个函数的粒度都很小(finely grained),那么函数之间彼此复用的机会就更大;其次,这会使高层函数码读起来就像一系列注释;再者,如果函数都是细粒度,那么函数的覆写(overridden)也会更容易些。
的确,如果你习惯看大型函数,恐怕需要一段时间才能适应这种新风格。而且只有当你能给小型函数很好地命名时,它们才能真正起作用,所以你需要在函数名称下点功夫。人们有时会问我,一个函数多长才算合适?在我看来,长度不是问题,关键在于函数名称和函数本体之间的语义距离(semantic distance )。如果提炼动作 (extracting )可以强化代码的清晰度,那就去做,就算函数名称比提炼出来的代码 还长也无所谓。
**作法(Mechanics)**
- 创造一个新函数,根据这个函数的意图来给它命名(以它「做什么」来命名, 而不是以它「怎样做」命名)。
- 即使你想要提炼(extract )的代码非常简单,例如只是一条消息或一个函数调用,只要新函数的名称能够以更好方式昭示代码意图,你也应该提炼它。但如果你想不出一个更有意义的名称,就别动。
- 将提炼出的代^码从源函数(source)拷贝到新建的目标函数(target)中。
- 仔细检查提炼出的代码,看看其中是否引用了「作用域(scope)限于源函数」的变量(包括局部变量和源函数参数)。
- 检查是否有「仅用于被提炼码」的临时变量(temporary variables )。如果有,在目标函数中将它们声明为临时变量。
- 检查被提炼码,看看是否有任何局部变量(local-scope variables )的值被它改变。如果一个临时变量值被修改了,看看是否可以将被提炼码处理为一个查询(query),并将结果赋值给相关变量。如果很难这样做,或如果被修改的 变量不止一个,你就不能仅仅将这段代码原封不动地离炼出来。你可能需要先使用 Split Temporary Variable,然后再尝试提炼。也可以使用Replace Temp with Query 将临时变量消灭掉(请看「范例」中的讨论)。
- 将被提炼码中需要读取的局部变量,当作参数传给目标函数。
- 处理完所有局部变量之后,进行编译。
- 在源函数中,将被提炼码替换为「对目标函数的调用」。
- 如果你将任何临时变量移到目标函数中,请检查它们原本的声明式是否在被提炼码的外围。如果是,现在你可以删除这些声明式了。
- 编译,测试。
**范例(examples):无局部变量(No Local Variables)**
在最简单的情况下,Extract Method 易如反掌。请看下列函数:
~~~
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);
}
~~~
我们可以轻松提炼出「打印banner」的代码。我只需要剪切、粘贴、再插入一个函数调用动作就行了:
~~~
void printOwing() {
Enumeration e = _orders.elements();
double outstanding = 0.0;
printBanner();
// 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 printBanner() {
// print banner
System.out.println ("**************************");
System.out.println ("***** Customer Owes ******");
System.out.println ("**************************");
}
~~~
**范例(Examples):有局部变量(Using Local Variables)**
果真这么简单,这个重构手法的困难点在哪里?是的,就在局部变量,包括传进源函数的参数和源函数所声明的临时变量。局部变量的作用域仅限于源函数,所以当我使用Extract Method 时,必须花费额外功夫去处理这些变量。某些时候它们甚至可能妨碍我,使我根本无法进行这项重构。
局部变量最简单的情况是:被提炼码只是读取这些变量的值,并不修改它们。这种情况下我可以简单地将它们当作参数传给目标函数。所以如果我面对下列函数:
~~~
void printOwing() {
Enumeration e = _orders.elements();
double outstanding = 0.0;
printBanner();
// 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() {
Enumeration e = _orders.elements();
double outstanding = 0.0;
printBanner();
// calculate outstanding
while (e.hasMoreElements()) {
Order each = (Order) e.nextElement();
outstanding += each.getAmount();
}
printDetails(outstanding);
}
void printDetails (double outstanding) {
System.out.println ("name:" + _name);
System.out.println ("amount" + outstanding);
}
~~~
必要的话,你可以用这种手法处理多个局部变量。
如果局部变量是个对象,而被提炼码调用了会对该对象造成修改的函数,也可以如法炮制。你同样只需将这个对象作为参数传递给目标函数即可。只有在被提炼码真的对一个局部变量赋值的情况下,你才必须采取其他措施。
**范例(Examples):对局部变量再赋值(Reassigning a Local Variable)**
如果被提炼码对局部变量赋值,问题就变得复杂了。这里我们只讨论临时变量的问题。如果你发现源函数的参数被赋值,应该马上使用Remove Assignments to Parameters。
被赋值的临时变量也分两种情况。较简单的情况是:这个变量只在被提炼码区段中使用。果真如此,你可以将这个临时变量的声明式移到被提炼码中,然后一起提炼出去。另一种情况是:被提炼码之外的代码也使用了这个变量。这又分为两种情况: 如果这个变量在被提炼码之后未再被使用,你只需直接在目标函数中修改它就可以了;如果被提炼码之后的代码还使用了这个变量,你就需要让目标函数返回该变量改变后的值。我以下列代码说明这几种不同情况:
~~~
void printOwing() {
Enumeration e = _orders.elements();
double outstanding = 0.0;
printBanner();
// calculate outstanding
while (e.hasMoreElements()) {
Order each = (Order) e.nextElement();
outstanding += each.getAmount();
}
printDetails(outstanding);
}
~~~
现在我把「计算」代码提炼出来:
~~~
void printOwing() {
printBanner();
double outstanding = getOutstanding();
printDetails(outstanding);
}
double getOutstanding() {
Enumeration e = _orders.elements();
double outstanding = 0.0;
while (e.hasMoreElements()) {
Order each = (Order) e.nextElement();
outstanding += each.getAmount();
}
return outstanding;
}
~~~
Enumeration变量 e只在被提炼码中用到,所以我可以将它整个搬到新函数中。double变量outstanding在被提炼码内外都被使用到,所以我必须让提炼出来的新函数返回它。编译测试完成后,我就把回传值改名,遵循我的一贯命名原则:
~~~
double getOutstanding() {
Enumeration e = _orders.elements();
double result = 0.0;
while (e.hasMoreElements()) {
Order each = (Order) e.nextElement();
result = each.getAmount();
}
return result;
}
~~~
本例中的outstanding变量只是很单纯地被初始化为一个明确初值,所以我可以只在新函数中对它初始化。如果代码还对这个变量做了其他处理,我就必须将它的值作为参数传给目标函数。对于这种变化,最初代码可能是这样:
~~~
void printOwing(double previousAmount) {
Enumeration e = _orders.elements();
double outstanding = previousAmount * 1.2;
printBanner();
// calculate outstanding
while (e.hasMoreElements()) {
Order each = (Order) e.nextElement();
outstanding += each.getAmount();
}
printDetails(outstanding);
}
~~~
提炼后的代码可能是这样:
~~~
void printOwing(double previousAmount) {
double outstanding = previousAmount * 1.2;
printBanner();
outstanding = getOutstanding(outstanding);
printDetails(outstanding);
}
double getOutstanding(double initialValue) {
double result = initialValue;
Enumeration e = _orders.elements();
while (e.hasMoreElements()) {
Order each = (Order) e.nextElement();
result += each.getAmount();
}
return result;
}
~~~
编译并测试后,我再将变量outstanding初始化过程整理一下:
~~~
void printOwing(double previousAmount) {
printBanner();
double outstanding = getOutstanding(previousAmount * 1.2);
printDetails(outstanding);
}
~~~
这时候,你可能会问:『如果需要返回的变量不止一个,又该怎么办呢?』
你有数种选择。最好的选择通常是:挑选另一块代码来提炼。我比较喜欢让每个函 数都只返回一个值,所以我会安排多个函数,用以返回多个值。如果你使用的语言支持「输出式参数」(output parameters),你可以使用它们带回多个回传值。但我还是尽可能选择单一返回值。
临时变量往往为数众多,甚至会使提炼工作举步维艰。这种情况下,我会尝试先运用 Replace Temp with Query 减少临时变量。如果即使这么做了提炼依旧困难重重,我就会动用 Replace Method with Method Object,这个重构手法不在乎代码中有多少临时变量,也不在乎你如何使用它们。
- 译序 by 侯捷
- 译序 by 熊节
- 序言
- 前言
- 章节一 重构,第一个案例
- 起点
- 重构的第一步
- 分解并重组statement()
- 运用多态(Polymorphism)取代与价格相关的条件逻辑
- 结语
- 章节二 重构原则
- 何谓重构
- 为何重构
- 「重构」助你找到臭虫(bugs)
- 何时重构
- 怎么对经理说?
- 重构的难题
- 重构与设计
- 重构与性能(Performance)
- 重构起源何处?
- 章节三 代码的坏味道
- Duplicated Code(重复的代码)
- Long Method(过长函数)
- Large Class(过大类)
- Long Parameter List(过长参数列)
- Divergent Change(发散式变化)
- Shotgun Surgery(散弹式修改)
- Feature Envy(依恋情结)
- Data Clumps(数据泥团)
- Primitive Obsession(基本型别偏执)
- Switch Statements(switch惊悚现身)
- Parallel Inheritance Hierarchies(平行继承体系)
- Lazy Class(冗赘类)
- Speculative Generality(夸夸其谈未来性)
- Temporary Field(令人迷惑的暂时值域)
- Message Chains(过度耦合的消息链)
- Middle Man(中间转手人)
- Inappropriate Intimacy(狎昵关系)
- Alternative Classes with Different Interfaces(异曲同工的类)
- Incomplete Library Class(不完美的程序库类)
- Data Class(纯稚的数据类)
- Refused Bequest(被拒绝的遗贈)
- Comments(过多的注释)
- 章节四 构筑测试体系
- 自我测试代码的价值
- JUnit测试框架
- 添加更多测试
- 章节五 重构名录
- 重构的记录格式
- 寻找引用点
- 这些重构准则有多成熟
- 章节六 重新组织你的函数
- Extract Method(提炼函数)
- Inline Method(将函数内联化)
- Inline Temp(将临时变量内联化)
- Replace Temp with Query(以查询取代临时变量)
- Introduce Explaining Variable(引入解释性变量)
- Split Temporary Variable(剖解临时变量)
- Remove Assignments to Parameters(移除对参数的赋值动作)
- Replace Method with Method Object(以函数对象取代函数)
- Substitute Algorithm(替换你的算法)
- 章节七 在对象之间搬移特性
- Move Method(搬移函数)
- Move Field(搬移值域)
- Extract Class(提炼类)
- Inline Class(将类内联化)
- Hide Delegate(隐藏「委托关系」)
- Remove Middle Man(移除中间人)
- Introduce Foreign Method(引入外加函数)
- Introduce Local Extension(引入本地扩展)
- 章节八 重新组织数据
- Self Encapsulate Field(自封装值域)
- Replace Data Value with Object(以对象取代数据值)
- Change Value to Reference(将实值对象改为引用对象)
- Replace Array with Object(以对象取代数组)
- Replace Array with Object(以对象取代数组)
- Duplicate Observed Data(复制「被监视数据」)
- Change Unidirectional Association to Bidirectional(将单向关联改为双向)
- Change Bidirectional Association to Unidirectional(将双向关联改为单向)
- Replace Magic Number with Symbolic Constant(以符号常量/字面常量取代魔法数)
- Encapsulate Field(封装值域)
- Encapsulate Collection(封装群集)
- Replace Record with Data Class(以数据类取代记录)
- Replace Type Code with Class(以类取代型别码)
- Replace Type Code with Subclasses(以子类取代型别码)
- Replace Type Code with State/Strategy(以State/strategy 取代型别码)
- Replace Subclass with Fields(以值域取代子类)
- 章节九 简化条件表达式
- Decompose Conditional(分解条件式)
- Consolidate Conditional Expression(合并条件式)
- Consolidate Duplicate Conditional Fragments(合并重复的条件片段)
- Remove Control Flag(移除控制标记)
- Replace Nested Conditional with Guard Clauses(以卫语句取代嵌套条件式)
- Replace Conditional with Polymorphism(以多态取代条件式)
- Introduce Null Object(引入Null 对象)
- Introduce Assertion(引入断言)
- 章节十一 处理概括关系
- Pull Up Field(值域上移)
- Pull Up Method(函数上移)
- Pull Up Constructor Body(构造函数本体上移)
- Push Down Method(函数下移)
- Push Down Field(值域下移)
- Extract Subclass(提炼子类)
- Extract Superclass(提炼超类)
- Extract Interface(提炼接口)
- Collapse Hierarchy(折叠继承关系)
- Form Template Method(塑造模板函数)
- Replace Inheritance with Delegation(以委托取代继承)
- Replace Delegation with Inheritance(以继承取代委托)
- 章节十二 大型重构
- 这场游戏的本质
- Tease Apart Inheritance(梳理并分解继承体系)
- Convert Procedural Design to Objects(将过程化设计转化为对象设计)
- Separate Domain from Presentation(将领域和表述/显示分离)
- Extract Hierarchy(提炼继承体系)
- 章节十三 重构,复用与现实
- 现实的检验
- 为什么开发者不愿意重构他们的程序?
- 现实的检验(再论)
- 重构的资源和参考资料
- 从重构联想到软件复用和技术传播
- 结语
- 参考文献
- 章节十四 重构工具
- 使用工具进行重构
- 重构工具的技术标准(Technical Criteria )
- 重构工具的实用标准(Practical Criteria )
- 小结
- 章节十五 集成
- 参考书目