重构·改善既有代码的设计.03之重构手法(上)
Posted 有一只柴犬
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了重构·改善既有代码的设计.03之重构手法(上)相关的知识,希望对你有一定的参考价值。
1. 前言
之前的重构系列中,介绍了书中提到的重构基础,以及识别代码的坏味道。今天继续第三更,讲述那些重构手法(上)。看看哪些手法对你的项目能有所帮助…
2. 重新组织函数
对函数进行整理,使之更恰当的包装代码。
1、Extract Method 提炼函数。
改造前:
void printInfoAndDetail()
this.printInfo();
System.out.println("this is detail name:" + _name);
System.out.println("this is detail account:" + _account);
改造后:
void printInfoAndDetail()
this.printInfo();
this.printDetail();
void printDetail()
System.out.println("this is detail name:" + _name);
System.out.println("this is detail account:" + _account);
动机:
控制函数的粒度,函数粒度很小,那么被复用的机会就更大;其次会使高层函数读起来就像一系列注释,再次,如果函数都是细粒度,那么函数的覆盖也会更容易。
一个函数多长才算合适?其实长度不是关键问题,关键在于函数名和函数本体之间的语义距离。
做法:
1、创造一个新函数,根据这个函数意图来命名(以它”做什么“来命名,而不是”怎样做“命名)。
只要新的函数名能够以更好的方式昭示代码意图,你也应该提炼他(就算代码只是一条消息,或一个函数调用)。但如果你想不出一个更有意义的名称,就别动。
2、将提炼出来额代码从源函数复制到新建的目标函数中。
3、检查变量。检查提炼出的代码是否引用了源代函数的局部变量或参数。以被提炼函数中是否含有临时变量。
难点:
这个重构手法的难点就在于局部变量的控制,包括传进源函数的参数和源函数所有声明的临时变量。
2、Inline Method 内联函数。
改造前:
int getRating()
return isGe5() ? 2 : 1;
boolean isGe5()
return _num >= 5;
改造后:
int getRating()
return _num >= 5 ? 2 : 1;
动机:
移除非必要的间接层。当然间接层有其价值,但不是所有的间接层都有价值,可以去除那些无用的间接层。
做法:
1、检查函数,确定他不具备多态性。如果有子类继承了这个函数,那就不能将此函数内联。因为子类无法覆盖一个根本不存在的函数。如例子中,子类可以重写isGe5(),但内敛之后的_num > 5 ? 2 : 1是无法重写的,除非你重写了getRating()。
2、找出函数的所有被调用点,将这个函数的所有被调用点都替换为函数本体。
3、Inline Temp 内联临时变量。
改造前:
double price = order.price();
return price > 1000;
改造后:
return order.price() > 1000
4、Replace Temp With Query 以查询取代临时变量。
改造前:
double price = _qu * _item;
if(price > 1000)
return price * 0.95;
else
return price * 0.98;
改造后:
if(getPrice() > 1000)
return getPrice() * 0.95;
else
return getPrice() * 0.98;
double getPrice()
return _qu * _item;
5、Introduce Explaining Variable 引入解释性变量。
改造前:
if((platform.indexOf("mac") > -1)
&& (platform.indexOf("ie") > -1)
&& resize > 0
)
// todo...
改造后:
final boolean isMac = (platform.indexOf("mac") > -1;
final boolean isIe = (platform.indexOf("ie") > -1;
final boolean resized = resize > 0;
if( && isIe && resized)
// todo...
6、Split Temporary Variable 分解临时变量。
改造前:
double temp = 2 * (_h + _w);
System.out.println(temp);
temp = _h * _w;
System.out.println(temp);
改造后:
final double temp = 2 * (_h + _w);
System.out.println(temp);
final double area = _h * _w;
System.out.println(area);
7、Remove Assignments to Parameters 移除对参数的赋值。
改造前:
int discount(int inputVal)
if(inputVal > 50) inputVal -= 2;
改造后:
int discount(int inputVal)
int result = inputVal;
if(inputVal > 50) result -= 2;
3. 在对象之间搬移特性
“决定把责任放在哪儿”。
1、Move Method 搬移函数。
如果一个类有太多的行为,或如果一个类于另一个类有太多合作而形成高度耦合,尝试搬移函数。将旧函数变成一个单纯的委托函数,或是将旧函数完全移除。
改造前:
class Account
private AccountType _type;
private int _dayOverdrawn;
double overdraftCharge()
if(_type.isPremium())
double result = 10;
if(_dayOverdrawn > 7) result += (_dayOverdrawn - 7) * 0.85;
return result;
else
return _dayOverdrawn * 1.75;
改造后:
class Account
private AccountType _type;
private int _dayOverdrawn;
double overdraftCharge()
return _type.overdraftCharge(_dayOverdrawn);
class AccountType
double overdraftCharge(int daysOverdrawn)
if(isPremium())
double result = 10;
if(dayOverdrawn > 7) result += (dayOverdrawn - 7) * 0.85;
return result;
else
return dayOverdrawn * 1.75;
2、Move Field 搬移字段。
如果一个字段,在其所驻类之外的另一个类中有更多函数使用了它,就要考虑搬移这个字段。这里的使用可能是设值,取值函数间接进行的。
改造前:
class Account
private AccountType _type;
private int _rate;
double overdraftCharge(double amount, int days)
return _rate * amount * days / 365;
改造后:
class Account
private AccountType _type;
double overdraftCharge()
return _type.getRate() * amount * days / 365;
class AccountType
private double _rate;
void setRate(double r)
this._rate = r;
void getRate()
return _rate;
3、Extract Class 提炼类。
建立一个新类,将相关的字段和函数从旧类搬移到新类。一个类应该是一个清楚的抽象,处理一些明确的责任。
改造前:
class Account
private String personName;
private String personPhone;
private double money;
public String getAccountInfo()
return personName + ",联系方式:" + personPhone + ",余额:" + money;
改造后:
class Account
private Person person = new Person();
private double money;
public String getAccountInfo()
return person.getPersonName() + person.personPhone() + ",余额:" + money;
class Person
private String personName;
private String personPhone;
public String getPersonName()
return "联系人:" + personName;
public String getPersonPhone()
return "联系方式:" + personPhone;
4、Inline Class 将类内联化。
将这个类的所有特性搬移到另一个类中,然后移除原类。与Extract Class相反。
5、Hide Delegate 隐藏“委托关系”。
“封装”即使不是对象的最关键特性,也是最关键特性之一。“封装”意味着每个对象都应该尽可能少了解系统的其他部分。
改造前:
class Person
private Department department;
public Department getDepartment()
return department;
class Department
private Person manager;
public Person getManager()
return manager;
// 如果客户希望知道某人的经理是谁,那他的调用关系是:
xxx.getDepartment().getManager();
// 暴露了部门和经理的委托关系
改造后:
class Person
private Department department;
public Department getDepartment()
return department;
public Person getManager()
return department.getManager();
class Department
private Person manager;
public Person getManager()
return manager;
// 如果客户希望知道某人的经理是谁,那他的调用关系是:(隐藏了Department)
xxx.getManager();
6、Remove Middle Man 移除中间人。
某个类做了过多的简单委托动作。
7、Introduce Foreign Method 引入外加函数。
当你需要为提供服务的类增加一个函数,但你无法修改这个类。如果你只使用这个函数一次,那么额外编码工作没什么大不了,升值可能根本不需要原本提供服务的那个类。然而,如果你需要多次使用这个函数,就得不断重复这些代码。重复代码是软件万恶之源。
改造前:
Date newStart = new Date(pre.getYear(), pre.getMethod(), pre.getDate() + 1);
改造后:
Date newStart = nextDay(pre);
private static Date nextDay(Date arg)
return new Date(arg.getYear(), arg.getMethod(), arg.getDate() + 1);
如真实项目中的案例:
BeanUtil.copyProperties(),原始方法该行为需要抛异常,且被建议不再使用该方法进行bean复制。
于是引入外加函数:
class BeanUtilExt
public static void copyProperties(Object target, Object source)
try
BeanUtil.copyProperties()
catch (Exception)
// ignored...
这种方式个人不推荐。
8、Introduce Local Extension 引入本地扩展。
当你需要为提供服务的类提供一些额外函数,但你无法修改这个类。
4. 重新组织数据
1、Self Encapsulate Field 自封装字段。
改造前:
private int _low, _high;
boolean includes(int arg)
return arg >= _low && arg <= _high;
改造后:
private int _low, _high;
boolean includes(int arg)
return arg >= getLow() && arg <= getHigh();
int getLow()
return _low;
int getHigh()
return _high;
直接访问变量好处:代码容易阅读。
间接访问变量好处:子类可以通过重写(覆盖)一个函数而改变获取数据的途径。
2、Replace Data Value with Object 以对象取代数据值。
开发初期,你往往决定以简单的数据项表示简单的情况。但是,随着开发的进行,你可能会发现,这些简单的数据项不再那么简单了。比如你一开始会用字符串来表示“电话号码”,但是随后你会发现,电话号需要“格式化”,“抽取区号”之类的特殊行为。如果这样的数据项只有一两个,你还可以把相关函数放进数据项所属的对象里,但是Duplicate Code和Feature Envy很快就会表现出来。这时,你就应该将数值变为对象。
3、Change Value toReference 将值对象改为引用对象。
你从一个类衍生出许多批次相等的实例,希望将它们替换为同一个对象。
4、Change Reference to Value 将引用对象改为值对象。
你有一个引用对象,很小且不可变,而且不易管理。
5、Replace Array with Object 以对象取代数组。
你有一个数组,其中的元素各自代表不同的东西。
改造前:
String[] row = new String[3];
row[0] = "liver";
row[1] = "15";
改造后:
Performance row = new Performance();
row.setName("liver");
row.setWins(15);
6、Duplicate Observed Data 复制“被监视数据”。
有一些领域数据置身于GUI组件中,而领域函数需要访问这些数据。
将该数据复制到一个领域对象中。建立一个Observer模式,用以同步领域对象和GUI对象内的重复数据。可以使用事件监听器,诸如JAVAFX中的Property。
7、Change Unidirectional Association to Bidirectional 将单向关联改为双向关联。
两个类都需要使用对方特性,但其间只有一条单向链接。
8、Change Bidirectional Association to Unidirectional 将双向关联改为单向关联。
两个类之间有双向关联,但其中一个类如今不再需要另一个类的特性。
9、Replace Magic Number with Symbolic Constant 以字面常量取代魔法值。
改造前:
double count(double a, double b)
return a * 0.95 * b;
改造后:
double count(double a, double b)
return a * RATE_CONSTANT * b;
static final double RATE_CONSTANT = 0.95;
10、Encapsulate Field 封装字段。
即面向对象的首要原则之一:封装,或称为“数据隐藏”。
改造前:
public String _name;
改造后:
private String _name;
public String getName()
return _name;
public void setName(String name)
this._name = name;
11、Encapsulate Collection 封装集合。
让这个函数返回该集合的一个只读副本,并在这个类中提供添加/移除集合元素的函数。
改造前:
class Person
List<String> classes;
public List<String> getClasses()
return classes;
public void setClasses(List<String> cls)
this.classes = cls;
改造后:
class Person
List<String> classes;
public List<String> getClasses()
return classes;
// setter方法隐藏,避免用户修改集合内容而一无所知
private void setClasses(List<String> cls)
this.classes = cls;
public void addClass(String cls)
classes.add(cls);
public void removeClass(String cls)
classes.remove(cls);
12、Replace Record with Data Class 以数据类取代记录。
主要用来应对传统编程环境中的记录结构。
13、Replace Type Code with Class 以类取代类型码。
类中有一个数值类型码,但它并不影响类的行为。
改造前:
class Person
public static final int O = 0;
public static final int A = 1;
public static final int B = 2;
public static final int AB = 3;
@Getter
@Setter
private int _bloodGroup;
public Person(int bloodGroup)
_bloodGroup = bloodGroup;
改造后:
class Person
public static final int O = BloodGroup.O.getCode();
public static final int A = BloodGroup.A.getCode();
public static final int B = BloodGroup.B.getCode();
public static final int AB = BloodGroup.AB.getCode();
@Getter
private int _bloodGroup;
public Person(int bloodGroup)
_bloodGroup = BloodGroup.code(bloodGroup);
public void setBloodGroup(int arg)
_bloodGroup = BloodGroup.code(arg)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以上是关于重构·改善既有代码的设计.03之重构手法(上)的主要内容,如果未能解决你的问题,请参考以下文章
重构改善既有代码设计--重构手法02:Inline Method (内联函数)& 03: Inline Temp(内联临时变量)
重构改善既有代码设计--重构手法14:Hide Delegate (隐藏委托关系)