重构·改善既有代码的设计.04之重构手法(下)完结
Posted 有一只柴犬
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了重构·改善既有代码的设计.04之重构手法(下)完结相关的知识,希望对你有一定的参考价值。
1. 前言
本文是代码重构系列的最后一篇啦。前面三篇《重构·改善既有代码的设计.01之入门基础》、《重构·改善既有代码的设计.02之代码的“坏味道”》、《重构·改善既有代码的设计.03之重构手法(上)》介绍了基础入门,代码异味,还有部分重构手法。今天继续总结重构手法下篇,从条件表达式、函数调用、以及类继承关系上阐述了各种重构手法,希望对项目能有所帮助。另外本文更新后该系列就完结了,感谢各位看官的指点。
2. 简化条件表达式
“分支逻辑”和“操作细节”分离。
1、Decompose Conditional 分解条件表达式。
复杂的条件语句(if-then-else)。
改造前:
if(date.before(SUMMER_START) || date.after(SUMMER_END))
charge = quantity * _winterRate * _winterServiceCharge;
else
charge = quantity * _summerRate;
改造后:
if(notSummer(date))
charge = winterCharge(quantity);
else
charge = summerCharge(quantity);
private boolean notSummer(Date date)
return date.before(SUMMER_START) || date.after(SUMMER_END);
private double summerCharge(int quantity)
return quantity * _summerRate;
private double winterCharge(int quantity)
return quantity * _winterRate * _winterServiceCharge;
2、Consolidate Conditional Expression 合并条件表达式。
有一系列条件测试,都得到相同结果。将这些测试合并为一个条件表达式,并将这个条件表达式提炼成一个独立函数。
改造前:
double disabilityAmount()
if(_seniority < 2)
return 0;
if(_monthDisabled > 12)
return 0;
if(_isPartTime)
return 0;
// todo...
改造后:
double disabilityAmount()
if((_seniority < 2) || (_monthDisabled > 12) || _isPartTime)
return 0;
// todo...
或:
double disabilityAmount()
if(isDisability())
return 0;
// todo...
boolean isDisability()
return (_seniority < 2) || (_monthDisabled > 12) || _isPartTime;
3、Consolidate Duplicate Conditional Fragments 合并重复的条件片段。
条件表达式的每个分支上有着相同的一段代码。
改造前:
if(isSpecialDeal())
total = price * 0.95;
send();
else
total = price * 0.98;
send();
改造后:
if(isSpecialDeal())
total = price * 0.95;
else
total = price * 0.98;
send();
4、Remove Control Flag 移除控制标记。
循环体中,通常需要判断何时停止条件检查。有时会引入某个控制变量来起到循环判断的作用。建议以break或continue或return 语句取代控制标记。
改造前:
void check(String[] person)
boolean found = false; // 控制标记
for(int i = 0; i < person.length; i++)
if(!found)
if(person[i] == "tom")
found = true;
sendAlert();
if(person[i] == "jose")
found = true;
sendAlert();
改造后:
void check(String[] person)
boolean found = false; // 控制标记
for(int i = 0; i < person.length; i++)
if(!found)
if(person[i] == "tom")
sendAlert();
break;
if(person[i] == "jose")
sendAlert();
break;
5、Replace Nested Conditional with Guard Clauses 以卫语句取代嵌套条件表达式。
条件表达式中,如果两条分支都是正常行为,使用形如if…else…的条件表达式;如果某个条件极为罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回(如参数校验判断)。这样的单检查称为“卫语句”。
卫语句要么就从函数中返回,要么就抛出一个异常。
改造前:
double getPayment()
double result;
if(_isDead)
result = deadAmount();
else
if(_isSeparated)
result = separatedAmount();
else
if(_isRetired)
result = retiredAmount();
else
result = normalAmount();
return result;
改造后:
double getPayment()
if(_isDead)
return deadAmount();
if(_isSeparated)
return separatedAmount();
if(_isRetired)
return retiredAmount();
return normalAmount();
6、Replace Conditional with Polymorphism 以多态取代条件表达式。
改造前:
class Employee
private EmployeeType _type;
int payment()
switch(getType())
case EmployeeType.ENGINEER:
return _monthlySalary;
case EmployeeType.SALESMAN:
return _monthlySalary + _commission;
case EmployeeType.MANAGER:
return _monthlySalary + _bonus;
default:
throw new RuntimeException("error");
int getType()
return _type.getTypeCode();
abstract class EmployeeType
abstract int getTypeCode();
class Engineer extends EmployeeType
int getTypeCode()
return EmployeeType.ENGINEER;
class Salesman extends EmployeeType
int getTypeCode()
return EmployeeType.SALESMAN;
class Manager extends EmployeeType
int getTypeCode()
return EmployeeType.MANAGER;
改造后:
class Employee
private EmployeeType _type;
int payment()
return _type.payment();
abstract class EmployeeType
abstract int payment(Employee emp);
class Engineer extends EmployeeType
int payment(Employee emp)
return emp.getMonthlySalary();
class Salesman extends EmployeeType
int payment(Employee emp)
return emp.getMonthlySalary() + emp.getCommission();
class Manager extends EmployeeType
int payment(Employee emp)
return emp.getMonthlySalary() + emp.getBonus();
7、Introduce Null Object 引入Null对象。
将null替换为null对象。
改造前:
class Site
private Customer _customer;
Customer getCustomer()
return _customer;
class Customer
public Stirng getName() ...
public BillingPlan getPlan()...
// 调用
Customer customer = site.getCustomer();
if(customer == null)
plan = BillingPlan.basic();
else
plan = customer.getPlan();
String customerName;
if(customer == null)
customerName = "default";
else
customerName = customer.getName();
改造后:
class Site
private Customer _customer;
Customer getCustomer()
return _customer == ull ? Customer.newNull() : _customer;
class Customer
public Stirng getName() ...
public BillingPlan getPlan()...
public boolean isNull()
return false;
static Customer newNull()
return new NullCustomer();
// 定义NullCustomer空对象
class NullCustomer extends Customer
public boolean isNull()
return true;
// 调用
Customer customer = site.getCustomer();
if(customer.isNull())
plan = BillingPlan.basic();
else
plan = customer.getPlan();
String customerName;
if(customer.isNull())
customerName = "default";
else
customerName = customer.getName();
8、Introduce Assertion 引入断言。
一段代码需要对程序状态做出某种假设。以断言明确表现这种假设。
改造前:
double getExpenseLimit()
return (_expenseLimit != NULL_EXPENSE) ? _expenseLimit : _primaryProject.getMemberExpenseLimit();
改造后:
double getExpenseLimit()
Assert.isTrue(_expenseLimit != NULL_EXPENSE || _primaryProject != null);
return (_expenseLimit != NULL_EXPENSE) ? _expenseLimit : _primaryProject.getMemberExpenseLimit();
断言,请不要用它来检查“你认为应该为真”的条件,请只使用它来检查“一定必须为真”的条件。请勿滥用。
如果断言所指示的约束条件不能满足,代码是否仍能正常运行? 如果可以,就把断言去掉。
3. 简化函数调用
容易被理解和被使用的接口,是开发良好面向对象软件的关键。
1、Rename Method 函数改名。
将复杂的处理过程分解成小函数。但是如果做的不好,会使你费尽周折却弄不清楚这些小函数各自的用途。要避免这种麻烦,关键在于给函数起一个好名称。
1、尽可能起一个良好名称的函数,顾名思义表达该函数的作用,而不是表达该函数如何做。
2、合理安排函数签名,如果重新安排参数顺序,能够帮助提供代码的清晰度。
2、Add Parameter 添加参数。
为函数添加一个对象参数,让该对象带进函数所需信息。
动机:
你必须修改一个函数,而修改后的函数需要一些过去没有的信息,因此你需要给该函数添加一个参数。
改造前:
double getExpenseLimit(double limit)
// todo...
// 需要添加一个参数
double getExpenseLimit(double limit, Date date)
// todo...
改造后:
double getExpenseLimit(ExpenseLimit limitObj)
// todo...
double limit = limitObj.getLimit();
Date date = limitObj.getDate();
class ExpenseLimit
double limit;
Date date;
但是需要警惕引用传递。其实我并不推荐整个对象传参。当你传整个参数时,对于这个函数你不能准确的说出这个函数所使用的参数。有可能对象包含了5个参数,而你理论上只需要3个。 这时候宁可将参数依次卸载参数列表中。不过所带来的影响是代码参数过长。如果过长,也是不太友好的。
推荐:适当使用参数列表和对象参数,必要时可以进行函数重载更简洁说明函数意图。比如:
double getExpenseLimit(double limit)
this.getExpenseLimit(limit, new Date());
double getExpenseLimit(double limit, Date date)
ExpenseLimit limitObj = new ExpenseLimit(limit, date);
this.getExpenseLimit(limitObj);
double getExpenseLimit(ExpenseLimit limitObj)
// todo...
double limit = limitObj.getLimit();
Date date = limitObj.getDate();
class ExpenseLimit
double limit;
Date date;
3、Remove Parameter 移除参数。
移除函数体无用参数。
改造前:
double getExpenseLimit(double limit, Date date)
return limit * 0.98;
改造后:
double getExpenseLimit(double limit)
return limit * 0.98;
4、Separate Query from Modifier 将查询函数和修改函数分离。
某个函数既返回对象状态值,又修改对象状态。
5、Parameterize Method 令函数携带参数。
若干函数做了类似的工作,但在函数本体中却包含了不同的值。
改造前:
class Employee
void tenPercentRaise()
salary *= 1.1;
void fivePercentRaise()
salary *= 1.05;
改造后:
class Employee
void raise(double factor)
salary *= (1 + factor);
6、Replace Parameter with Explicit Methods 以明确函数取代参数。
有一个函数,其中完全取决于参数值而采取不同行动。
改造前:
void setValue(String name, int value)
if(name.equals("height"))
_height = value;
return ;
if(name.equals("width"))
_width = value;
return ;
改造后:
void setHeight(int value)
_height = value;
void setWidth(int value)
_width = value;
7、Preserve Whole Object 保持对象完整。
从某个对象中取出若干值,将它们作为某一个函数调用时的参数。
改造前:
int low = daysTempRange().getLow();
int high = daysTempRange().getHigh();
withinPlan = plan.withinRange(low, high);
改造后:
withinPlan = plan.withinRange(daysTempRange());
注:
该方法总有两面。如果你传的是数值,被调用函数就只依赖于这些数值,而不依赖它们所属的对象。但如果你传递的是整个对象,被调用函数所在的对象就需要依赖参数对象。如果这样,会使你的依赖结构恶化,那么就不该使用该方法。
8、Replace Parameter with Methods 以函数取代参数。
对象调用某个函数,并将所得结果作为参数,传递给另一个函数。而接受该参数的函数本身也能够调用前一个函数。
改造前:
int basePrice = _quantity * _itemPrice;
discountLevel = getDiscountLevel();
double finalPrice = discountedPrice(basePrice, discountLevel);
改造后:
int basePrice = _quantity * _itemPrice;
double finalPrice = discountedPrice(basePrice);
double discountedPrice(int basePrice)
discountLevel = getDiscountLe重构改善既有代码设计--重构手法01:Extract Method (提炼函数)
背景:
你有一段代码可以被组织在一起并独立出来。将这段代码放进一个独立函数,并让函数名称解释该函数的用途。
void PrintOwing(double amount)
{
PrintBanner();
//print details
Console.WriteLine("name:"+_name);
Console.WriteLine("amount:"+_amount);
}
void PrintOwing(double amount)
{
PrintBanner();
//print details
PrintDetails();
}
private void PrintDetails()
{
Console.WriteLine("name:" + _name);
Console.WriteLine("amount:" + _amount);
}
动机:
Extract Method (提炼函数)是最常用的重构手法之一。当看见一个过长的函数或者一段需要注释才能让人理解用途的代码,就应该将这段代码放进一个独立函数中。
简短而命名良好的函数的好处:首先,如果每个函数的粒度都很小,那么函数被复用的机会就更大;其次,这会使高层函数读起来就想一系列注释;再次,如果函数都是细粒度,那么函数的覆写也会更容易些。
一个函数多长才算合适?长度不是问题,关键在于函数名称和函数本体之间的语义距离。如果提炼可以强化代码的清晰度,那就去做,就算函数名称必提炼出来的代码还长也无所谓。
做法:
1、创造一个新函数,根据这个函数的意图对它命名(以它“做什么“命名,而不是以它“怎样做”命名)。
即使你想要提炼的代码非常简单,例如只是一条消息或一个函数调用,只要新函数的名称能够以更好方式昭示代码意图,你也应该提炼它。但如果你想不出一个更有意义的名称,就别动。
2、将提炼出的代码从源函数复制到新建的明白函数中。
3、仔细检查提炼出的代码,看看其中是否引用了“作用域限于源函数”的变量(包括局部变量和源函数参数)。
4、检查是否有“仅用于被提炼代码段”的临时变量。如果有,在目标函数中将它们声明为临时变量。
5、检查被提炼代码段,看看是否有任何局部变量的值被它改变。如果一个临时变量值被修改了,看看是否可以将被提炼代码处理为一个查询,并将结果赋值给修改变量。如果很难这样做,或如果被修改的变量不止一个,你就不能仅仅将这段代码原封不动提炼出来。你可能需要先使用 Split Temporary Variable (分解临时变量),然后再尝试提炼。也可以使用 Replace Temp with Query (以查询取代临时变量)将临时变量消灭掉。
6、将被提炼代码段中需要读取的局部变量,当做参数传给目标函数。
7、处理完所有局部变量后,进行编译。
8、在源函数中,将被提炼代码段替换给对目标函数的调用。
如果你将如何临时变量移到目标函数中,请检查它们原本的声明式是否在被提炼代码段的外围。如果是,现在可以删除这些声明式了。
9、编译,测试。
代码演示:
实例代码如下:
1 private string myName;
2 public void printPeople(int Age)
3 {
4 printFamily();
5 //
无数代码
//
6
7 //打印个人信息
8 Console.WriteLine("Name:" + myName);
9 Console.WriteLine("Age:" + Age);
10 }
重构后的代码如下:
1 private string myName;
2 public void printPeople(int Age)
3 {
4 printFamily();
5 //
无数代码
//
6 printMyInfo(Age);
7 }
8
9 void printMyInfo(int Age)
10 {
11 Console.WriteLine("Name:" + myName);
12 Console.WriteLine("Age:" + Age);
13 }
为什么要这样重构?当一个函数很大的时候,第一对代码的修改起来非常的不方便.
第二,会对你读代码有障碍,试想一下当你看到一个很多行代码的方法,你还有心情看下去吗?
第三,方法与方法之间的复用性会非常的好,方法的重写也会更容易些.
那么我们应该怎么做呢?
看第一个例子:
无局部变量的方法提炼.
1 void printOwing()
2 {
3 ArrayList al = myOrders.GetOrderList();
4 double outstanding = 0.0;
5
6 //打印头部信息
7 Console.WriteLine("*****************");
8 Console.WriteLine("**Customer Owes**");
9 Console.WriteLine("*****************");
10
11 //计算
12 foreach(Object o in al)
13 {
14 Order each = (Order)o;
15 outstanding += each.Amount;
16 }
17
18 //打印具体信息
19 Console.WriteLine("Name:" + myName);
20 Console.WriteLine("Age:" + age);
21 }
好了我们开始先提最简单的部分.提出后的代码如下:
1 void printOwing()
2 {
3 ArrayList al = myOrders.GetOrderList();
4 double outstanding = 0.0;
5
6 printBanner();
7
8 //计算
9 foreach(Object o in al)
10 {
11 Order each = (Order)o;
12 outstanding += each.Amount;
13 }
14
15 //打印具体信息
16 Console.WriteLine("Name:" + myName);
17 Console.WriteLine("Age:" + age);
18 }
19
20 void printBanner()
21 {
22 //打印头部信息
23 Console.WriteLine("*****************");
24 Console.WriteLine("**Customer Owes**");
25 Console.WriteLine("*****************");
26 }
最简单的提炼方法结束了.
下来我们看有局部变量的方法提炼.就拿上面的的代码开刀.
1 void printOwing()
2 {
3 ArrayList al = myOrders.GetOrderList();
4 double outstanding = 0.0;
5
6 printBanner();
7
8 //计算
9 foreach(Object o in al)
10 {
11 Order each = (Order)o;
12 outstanding += each.Amount;
13 }
14
15 printInfo(outstanding);
16 }
17
18 void printBanner()
19 {
20 //打印头部信息
21 Console.WriteLine("*****************");
22 Console.WriteLine("**Customer Owes**");
23 Console.WriteLine("*****************");
24 }
25
26 void printInfo(double OutStanding)
27 {
28 //打印具体信息
29 Console.WriteLine("Name:" + myName);
30 Console.WriteLine("Age:" + age);
31 }
我们再来看下对局部变量再赋值方法的提炼.继续拿上面代码开刀.
1 void printOwing()
2 {
3 double outstanding = GetOutStanding();
4
5 printBanner();
6
7 printInfo(outstanding);
8 }
9
10 void printBanner()
11 {
12 //打印头部信息
13 Console.WriteLine("*****************");
14 Console.WriteLine("**Customer Owes**");
15 Console.WriteLine("*****************");
16 }
17
18 void printInfo(double OutStanding)
19 {
20 //打印具体信息
21 Console.WriteLine("Name:" + myName);
22 Console.WriteLine("Age:" + age);
23 }
24
25 double GetOutStanding()
26 {
27 ArrayList al = myOrders.GetOrderList();
28 double outstanding = 0.0;
29 //计算
30 foreach(Object o in al)
31 {
32 Order each = (Order)o;
33 outstanding += each.Amount;
34 }
35 return outstanding
36 }
Extract Method方法讲解玩了.有人会问为什么要这样写?这样写的好处我没有看到啊.
那么现在有个这样的需求,我要设置outstanding的初始值,那么我们只要修改GetOutStanding方法,代码
如下:
1 double GetOutStanding(double previousAmount)
2 {
3 ArrayList al = myOrders.GetOrderList();
4 double outstanding = previousAmount;
5 //计算
6 foreach(Object o in al)
7 {
8 Order each = (Order)o;
9 outstanding += each.Amount;
10 }
11 return outstanding
12 }
主要方法修改如下:
1 void printOwing()
2 {
3 double outstanding = GetOutStanding(500.5);
4
5 printBanner();
6
7 printInfo(outstanding);
8 }
如果需求继续增加,我们修改起来是不是方便了许多?
读后感:
1.如果说没有任何局部变量,那么这个函数提炼就非常容易提炼.
2.如果说提炼的时候有局部变量,即用到了提炼函数之外的局部变量,那么如果仅仅是内部函数使用的,直接放到内部函数中;第二种,如果提炼的函数内部没有对此变量赋值的情况,仅仅是读取使用,那么直接从外面作为参数传递进来。
3.如果提炼的函数,不仅仅有局部变量,并且还要对其赋值,那么同样要看,这个局部变量是不是只是内部使用,如果只是内部使用,直接放进来,如果不是,那就说外面还要用到,那么需要经过提炼函数运算后,将值返回去.
以上是关于重构·改善既有代码的设计.04之重构手法(下)完结的主要内容,如果未能解决你的问题,请参考以下文章
重构改善既有代码设计--重构手法04:Replace Temp with Query (以查询取代临时变量)
重构改善既有代码设计--重构手法01:Extract Method (提炼函数)