核心内容源于《重构 改善既有代码的设计》(Refactoring Improving the Design of Existing Code——Martin Fowler著)。
Long Methods(过长函数)是很多问题的来源,因为过长的函数总是包含了太多的信息,而这些信息又被函数错综复杂的逻辑掩盖,不易鉴别。
对付过长函数,一项重要的重构手法就是Extract Method,它把一段代码从原先函数中提取出来,放进一个单独函数中。
而Extract Method最大的困难就是处理局部变量,而临时变量则是其中一个主要的困难源头。
处理局部变量时,我更信号运用 Replace Temp with Query 去掉所有可去掉的临时变量。
如果很多地方使用了某个临时变量,我就会先运用 Split Temporary Variable 将它变得比较容易替换。
但是有时候有些临时变量实在太过于混乱,难以替换,这个时候我就需要使用Replace Method with Method Object
当有参数时,可以考虑使用Remove Assignments to Paraments
当代码更加清晰的时候,可以考虑使用Substitute Algorithm引入更清晰的算法
逻辑总览:
目录
1.4Replace Temp with Query(以查询代替临时变量)
1.5Introduce Explaining Variable(引入解释性变量)
1.6Split Temporary Variable(分解临时变量)
1.7Remove Assignments to Parameters(移除对参数的赋值)
1.8Replace Method with Method Object(以函数对象取代函数)
1.1Extract Method(提炼函数)
将一段代码组织并独立出来,这是本章的核心方法,后续的1.2~1.9均是为了1.1服务的。
动机
当一个函数过长或者存在需要注释才能让人理解用途的代码,我们需要将这部分代码提炼到一个独立函数中。
优势:
①一个函数的粒度都很小,那么函数被复用的机会就更大
②如果函数是细粒度,那么覆写也会更容易些
总结:职责细化程度越合适,复用机会up,也更易修改(覆写)
要求:函数命名要规范
做法
最大的困难,其实是局部变量的问题
下面多种情况均以下面代码为基础进行重构
//重新组织函数
//Part1 Extract Method
void Widget::exampleFunction(vector<int> & test){
vector<int>test(10,0);
double outstanding = 0.0;
//print banner
cout<<"......................"<<endl;
cout<<"....Extract Method...."<<endl;
cout<<"......................"<<endl;
//calculate
for(auto item:test){
outstanding += item;
}
//print details
cout<<"....................."<<endl;
cout<<"....Result:"<<outstanding<<"..."<<endl;
cout<<"....................."<<endl;
}
情况一:无局部变量
我们可以轻易的将Print Banner部分提取出来,非常简单
//重新组织函数
//Part1 Extract Method
void Widget::exampleFunction(vector<int> & test){
double outstanding = 0.0;
//print banner
printBanner();
//calculate
for(auto item:test){
outstanding += item;
}
//print details
cout<<"....................."<<endl;
cout<<"....Result:"<<outstanding<<"..."<<endl;
cout<<"....................."<<endl;
}
void Widget::printBanner(){
//print banner
cout<<"......................"<<endl;
cout<<"....Extract Method...."<<endl;
cout<<"......................"<<endl;
}
这是最简单的情况, 无局部变量,提取Method没有参数也没有返回值。
情况二:有局部变量(被提炼代码只读不写)
存在局部变量最简单的情况,只是读取局部变量的值,但是并不对其进行修改
//重新组织函数
//Part1 Extract Method
void Widget::exampleFunction(vector<int> & test){
double outstanding = 0.0;
//print banner
printBanner();
//calculate
for(auto item:test){
outstanding += item;
}
//print details
printDetails(outstanding);
}
void Widget::printBanner(){
//print banner
cout<<"......................"<<endl;
cout<<"....Extract Method...."<<endl;
cout<<"......................"<<endl;
}
void Widget::printDetails(double outstanding){
//print details
cout<<"....................."<<endl;
cout<<"....Result:"<<outstanding<<"..."<<endl;
cout<<"....................."<<endl;
}
必要的话可以对多个局部变量进行此类操作。
情况三:对局部变量写操作
此处我们只讨论源函数的局部变量被进行写操作的情景,如果发现源函数的参数被赋值,应该马上使用 Remove Assignments to Parameters
对局部变量被进行写操作分两种情况:
①该局部变量只是在被提炼代码中使用,那么我们将该局部变量的声明移到被提炼代码段中即可。
②该局部变量在被提炼代码之外也有被使用,那么我们改变目标函数(被提炼代码组成的函数)的返回值即可。
依旧使用上面的例子:
//重新组织函数
//Part1 Extract Method
void Widget::exampleFunction(vector<int> & test){
//print banner
printBanner();
//calculate
double outstanding = calculateDetail(test);
//print details
printDetails(outstanding);
}
void Widget::printBanner(){
//print banner
cout<<"......................"<<endl;
cout<<"....Extract Method...."<<endl;
cout<<"......................"<<endl;
}
double Widget::calculateDetail(vector<int> & test){
double result = 0.0;
//calculate
for(auto item:test){
result += item;
}
return result;
}
void Widget::printDetails(double outstanding){
//print details
cout<<"....................."<<endl;
cout<<"....Result:"<<outstanding<<"..."<<endl;
cout<<"....................."<<endl;
}
那么如果情况再复杂一些,比如有多个返回值,可以安排多个函数。
如果临时变量过多,使得提炼工作举步维艰,那么我们可以先使用Replace Temp with Query减少临时变量。如果如此依旧很困难,我们就使用Replace Method with Method Object
1.2Inline Method(内联函数)
一个函数的本体与名称同样清楚易懂(避免过度使用Extract Method)
在函数调用点插入函数本体,然后移除该函数,因为本体已经足够清晰了,不需要再使用了。
int getRating(){
return (moreThanFiveLateDelivers())?2:1;
}
bool moreThanFiveLateDelivers(){
return _numberOfLateDelivers > 5;
}
应将此部分改为:
int getRating(){
return (_numberOfLateDelivers > 5)?2:1;
}
动机
Extract Method 过犹不及时,就会发生可以使用Inline Method
简短的函数确实更加清晰易读,非常明确的表明了动作的意图。
如果一个函数的名称和内容一样清晰,比如上面的示例,那么就可以直接将这部分函数拿掉,去掉这个间接层,比如Extract Methode本质是为了更加清晰的表明代码的意图,但是如果函数内部的代码内容已经足够清晰,比如_numberOfLateDelivers > 5,那么我们就不需要舍本逐末多此一举的去增加一个间接层。
如果别人使用了太多间接层,使得系统中的所有函数都似乎只对另一个函数的简单委托,造成程序员在这些委托动作之间晕头转向,那么我们通常使用Inline Methode来解决问题。
做法
①检查函数,确定不具有多态性(如果子类继承了这个函数,就不要将此类函数内联,因为子类无法覆写一个根本不存在的函数)
②找出这个函数的被调用点
③将这个函数的所有被调用点都替换成函数本体
④编译测试
⑤删除该函数的定义
1.3Inline Temp(内联临时变量)
你有一个临时变量,只被一个简单表达式赋值一次,而它妨碍了其他重构手法(局部变量的局限性,Martin Flower真的是对不必要的局部变量深恶痛绝)
将所有对该变量的引用动作,替换为对它赋值的那个表达式自身
double mbasePrice = basePrice();
return (mbasePrice > 1000);
重构为:
return (basePrice() > 1000);
动机
Inline Temp多半是作为Replace Temp with Query的一部分使用,所以真正的动机出现在后者哪儿
单独使用情况:如果一个临时变量没有影响到重构,将这个临时变量放在哪儿即可,但是如果影响到重构,比如Extract Method,那么就将其Inline Temp
做法
①检查给临时变量赋值的语句,确保等号右边的表达式没有副作用
②检查该临时变量是否真的只被赋值一次
③找到该临时变量的所有引用点,将它们替换为“为临时变量赋值”的表达式
1.4Replace Temp with Query(以查询代替临时变量)
消解临时变量(局部变量的局限性)
你的程序以一个临时变量保存某一表达式的运算结果
将这个表达式提炼到一个独立函数中。将这个临时变量的所有引用点替换为对新函数的调用。
double basePrice = _quantity * _itemPrice;
if(basePrice > 1000){
return basePrice * 0.95;
}
else {
return basePrice * 0.98;
}
重构为:
if(basePrice() > 1000){
return basePrice * 0.95;
}
else {
return basePrice * 0.98;
}
.....
double basePrice(){
return _quantity * _itemPrice;
}
动机
临时变量的问题在于,它们是暂时的,而且只能在所属函数内使用,由于临时变量只在所属函数内可见,所以它们会驱使你写出更长的函数因为这样你才会访问到需要的临时变量。如果把一个临时变量变成一个查询,同类中的函数都会获得这份信息。
Replace Temp with Query往往是运用Extract Methode之前必不可少的一个步骤。局部变量会使代码难以被提取,所以尽可能的将其替换为表达式。
如果情况比较复杂,可以使用Split Temporary Variable或Separate Query from Modifier使得情况变得简单。
做法
从下面函数开始:
double getPrice(){
int basePrice = _quatity * _itemPrice;
double discountFactor;
if( basePrice > 1000 ){
discountFactor = 0.95;
}
else {
discountFactor = 0.98;
}
return basePrice * discountFactor;
}
现在要将两个临时变量替换掉
double getPrice(){
return basePrice() * discountFactor();
}
int basePrice(){
return _quatity * _itemPrice;
}
double discountFactor(){
if( basePrice() > 1000 ){
return 0.95;
}
else {
return 0.98;
}
}
我们再次看getPrice函数,异常的清晰,整个过程中临时变量很少,几乎没有使用到,也非常有利于后期的维护和重构。
1.5Introduce Explaining Variable(引入解释性变量)
你有一个复杂的表达式
将复杂的表达式的一部分放在临时变量里面,以此变量名称来解释表达式用途
核心并不是引入局部变量,核心是整理代码段的思路,让这部分更加清晰
if( (platform.toUpperCase().indexOf("MAC") > -1 )&&
(brower.toUpperCase().indexOf("IE") > -1)&&
wasInitialized() && resize > 0){
//do something
}
重构为:
const bool isMacOS = platform.toUpperCase().indexOf("MAC") > -1;
const bool isIEbrowser = brower.toUpperCase().indexOf("IE") > -1;
const bool wasResized = resize > 0;
if( isMacOS && isIEbrowser && wasInitialized() && wasResized){
//do something
}
动机
有些表达式太过复杂不好阅读,这种情况下,我们使用临时变量来帮助理解。
但是毕竟此方法会引入临时变量,局限性太大,可以使用Extract Method
做法
double price(){
return _quantity * _itemPrice - Math.max(0,_quantity -500) * _itemPrice * 0.05 +
Math.min(_quantity * _itemPrice * 0.1,100.0);
}
现在对起使用Introduce Explaining Variable进行重构
double price(){
double basePrice = _quantity * _itemPrice;
double quantityDiscount = Math.max(0,quantity - 500)*_itemPrice *0.55;
double shipping = Math.min(basePrice * 0.1,100.0);
return basePrice - quantityDiscount * shipping;
}
我们使用Extract Method 进行重构,对比这两种方法的异同
double price(){
return basePrice() - quantityDiscount() * shipping();
}
//private
double quantityDiscount(){
return Math.max(0,_quantity - 500) * _itemPrice * 0.05;
}
double shipping(){
return Math.min(basePrice() * 0.1,100.0);
}
double basePrise(){
return _quantity * _itemPrice;
}
二者工作量一样
那么当直接使用Extract Method比较困难的时候,我们考虑先使用Introduce Explaining Variable来清理代码,搞清楚代码逻辑之后运用Replace Temp with Query把中间引入的那些解释性临时变量去掉。
如果可以使用Replace Method with Method Object,我们引入的临时变量也是有价值的。
1.6Split Temporary Variable(分解临时变量)
在非循环的情况下,某个临时变量被赋值超过一次
针对每次赋值,创造一个独立。对应的临时变量
单一职责原则
double temp = 2 * (_height + _width);
cout<<temp;
temp = _height * _width;
cout<<temp;
重构为:
double perimeter = 2 * (_height + _width);
cout<<perimeter;
double area = _height * _width;
cout<<area;
动机
临时变量的用途各不相同,有两种情况下,是可以进行多次赋值的:
①循环变量(loop variable)会根据循环的情况而改变
②结果收集变量(collecting temporary variable)将“整个函数的运算结果”而构成的某个值收集起来
其余情况,临时变量所承担的责任务必单一,如果被多次赋值,那么说明一个变量承担了过多责任,会令程序段逻辑不够清晰。
做法
double getDistanceTravelled(int time){
double result;
double acc = _primaryForce / _mass;
int primaryTime = Math.min(time,_delay);
result = 0.5 * acc * primaryTime * primaryTime;
int secondaryTime = tiem - _delay;
if(secondaryTime > 0){
double primaryVel = acc * _delay;
acc = (_primaryForce + _secondaryTime) / _mass;
result += primaryVel + secondaryTime + 0.5 * acc * secondaryTime * secondaryTime;
}
return result;
}
acc责任划分不清楚
double getDistanceTravelled(int time){
double result;
double PrimaryAcc = _primaryForce / _mass;
int primaryTime = Math.min(time,_delay);
result = 0.5 * PrimaryAcc * primaryTime * primaryTime;
int secondaryTime = tiem - _delay;
if(secondaryTime > 0){
double primaryVel = acc * _delay;
double secondaryAcc = (_primaryForce + _secondaryTime) / _mass;
result += primaryVel + secondaryTime + 0.5 * secondaryAcc * secondaryTime * secondaryTime;
}
return result;
}
1.7Remove Assignments to Parameters(移除对参数的赋值)
代码对一个参数进行赋值
以一个临时变量取代该参数的位置
int discount(int inputVal,int quantity,int yearToData){
if(inputVal > 50) {
inputVal -= 2;
}
重构为:
int discount(int inputVal,int quantity,int yearToData){
int result = inputVal;
if(inputVal > 50) {
result -= 2;
}
}
动机
在按值传递的情况下,对参数的修改,不会有任何的问题。
在按地址(引用)传递的情况下,那么会出问题。造成不必要的麻烦,使得代码不够清晰。
做法
int discount(int inputVal,int quantity,int yearToData){
if(inputVal > 50) {
inputVal -= 2;
}
if(quantity > 100){
inputVal -= 1;
}
if(yearToData > 10000){
inputVal -= 4;
}
return inputVal;
}
重构为:
int discount(const int inputVal,const int quantity,const int yearToData){
int result = inputVal;
if(inputVal > 50) {
result -= 2;
}
if(quantity > 100){
result -= 1;
}
if(yearToData > 10000){
inpuresulttVal -= 4;
}
return result;
}
在较长的函数中可以使用const关键字对参数进行修饰,以表示其不能被修改。
1.8Replace Method with Method Object(以函数对象取代函数)
你有一个大型函数,其中对局部变量的使用使你无法采用Extract Method
将这个函数放进一个单独类中,如此一来局部变量就成了类内的成员变量。然后你可以在同一个对象中将这个大型函数分解为多个小型函数。
class Order{
//...
double price(){
double primaryBasePrice;
double secondBasePrice;
double tertiaryBasePrice;
//long computation
}
}
动机
Martin Flower在重构这本书中不断在强调小型函数的优美动人,关于小型函数的优势,在Extract Method 中均有说明
当出现大量局部变量的时候,或许Replace Temp with Query可以解决一些问题,但是依旧杯水车薪的时候,我们就要使用Method Object这个方法了。
做法
class Account{
//...
int gamma(int inputVal,int quantity,int yearToDate){
int importantValue1 = (inputVal * quantity) + delta();
int importantValue2 = (inputVal * yearToDate) + 100;
if((yearToDate - importantValue1) > 100){
importantValue2 -= 20;
}
int importantValue3 = importantValue2 * 7;
return importantValue3 - 2 * importantValue1;
}
//....
void delta();
};
下面我们对上述内容进行重构
首先我们新建一个class,Gamma
class Gamma{
public:
Gamma(Acount source,int inputValArg,int quantityArg,int yearToDateArg){
_account = source;
inputVal = inputValArg;
quantity = quantityArg;
yearToDate = yearToDateArg;
}
~Gamma(){}
//将原来类(Account)中的gamma函数进行搬运
int compute(){
importantValue1 = (inputVal * quantity) + _account.delta();
importantValue2 = (inputVal * yearToDate) + 100;
if((yearToDate - importantValue1) > 100){
importantValue2 -= 20;
}
importantValue3 = importantValue2 * 7;
return importantValue3 - 2 * importantValue1;
}
private:
Account _account;
int inputVal;
int quantity;
int yearToDate;
int importantValue1;
int importantValue2;
int importantValue3;
};
注意Gamma构造函数的写法
之后对Account类中的gamma函数进行进一步的修改
class Account{
//...
int gamma(int inputVal,int quantity,int yearToDate){
Gamma tempObject = new Gamma(this,inputVal,quantity,yearToDate);
return tempObject.compute();
}
//....
void delta();
};
下来,我们不就可以直接对Gamma类中的compute函数使用Extract Method重构方法了。
1.9Substitute Algorithm(替换算法)
你想要把某个算法替换成另外一种更加清晰的算法
将函数本体替换成另一个算法
有多种方法可以解决问题,我敢打赌某些方法会比另一些简单。算法也是如此。
总结
小型函数复用性更强,更容易进行overload/override,这是对函数处理的核心方法,就是对一个大函数进行有限拆解(避免过犹不及,例如1.2),同时让代码逻辑性更加的清晰(如1.5,1.6,1.7),同时,尽可能的消除对局部变量的依赖和使用(如1.4)。