本文已参与「新人创作礼」活动.一起开启掘金创作之路。
重构:改善既有代码的设计之(一)
本次分享目录
重构背景
何谓重构
为何重构
何时重构
代码坏的味道
Duplicated Code(重复的代码)
Long Method(过长函数)
Long Parameter List(过长参数列)
Large Class(过大类)
分享之一总结四招:
一、重复的代码提炼成函数
二、把过长的函数变小
三、参数列太长或变化太频繁,参数对象化
四、类的代码行数太多,要考虑提炼子类。
\
重构背景
**
**
不改变软件可观察行为的前提下改善其内部结构。---Martin Fowler
通俗来讲:看起来没做啥调整,让系统继续更好的满足客户需求。同时,让这个系统能够多蹦跶几年。
系统已经在运行,不能停下。但是使用的轮子还是方型的轮子...性能效率都..
何谓重构
重构(名词)
对软件内部结构的一种调整,目的是在不改变"软件可观察行为"的前提下,提高其可理解性,降低其修改成本。简言之,在代码写好之后改进它的设计。
重构(Refactoring)通过调整程序代码改善软件的质量、性能,使其程序的设计模式和架构更趋合理,提高软件的扩展性和维护性。
\
重构(动词)
使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。\
为何重构? 代码的坏味道
**
**
Duplicated Code(重复的代码)
Long Method(过长函数)
Long Parameter List(过长参数列)
Large Class(过大类)
Divergent Change(发散式变化) 一个类受多种变化的影响
Shotgun Surgery(霰弹式改动) 一种变化引发多个类响应修改
Feature Envy(依恋情结):函数对某个类的兴趣高过对自己所处类的兴趣,是时候考虑这个函数到底应该放在什么位置了
Data Clumps(数据泥团) :两个类中相同的字段,许多函数中相同的参数,这时候就可以让他们拥有自己的类了,简而言之,类似的东西写一个类里
1、为了兼容的特殊逻辑,我们需要在其他场景中增加各种 if 判断来绕过这些逻辑。可读性很差。
2、业务策略越来越多,累计了几十个,当框架失去清晰的结构后,有些策略的实现开始变得定制化,缺少层次化的划分和可插拔式的抽象设计**
3、由于原程序结构不能满足用户的新需求、原程序有漏洞(bug)、原程序执行效率低下,性能不足以满足用户要求。
\
为何重构 ? 通过重构我们能得到什么?
1.重构能够改进软件设计
代码结构的流失是累积性的。越难看出代码的设计意图,就越难保护其中的设计,于是该设计就腐败的越快。
而经常性的重构可以帮助代码维持自己的形态和结构。
完成同样一件事,设计不良的程序往往需要更改代码,这通常是因为相同的代码在不同的地方做着同样的事。如果消除重复代码,你就可以确定所有的事物、行为在代码中只表述一次,这正是优秀设计的根本。
2.重构使软件更容易理解
重构会使代码渐趋简洁,越简洁就越容易理解,越容易理解就越容易修改…
3.重构能够找到代码中隐藏的bug
对代码进行重构,可以更深入的理解代码,搞清楚程序的结构,于是bug就被揪出来了。
重构能够帮助程序员更有效的写出更强健的代码。
4.重构提高编程速度
良好的设计是开发的根本,拥有良好的设计才可能做到快速开发。如果没有良好的设计,或许短时间内进展迅速,但是恶劣的设计很快就让你的速度慢下来。你会花费大量的时间进行调试,添加新功能难度越来越高,修改时间越来越长
\
何时重构?
事不过三,三则重构。
当我们第一次做一件事的时只管去做;第二次做类似的事就会产生反感,但无论如何还是可以去做;第三次再做类似的事,你就应该重构。
添加功能时重构
修补错误时重构
复审代码时重构
\
\
\
分享之一总结四招:
第一招 重复的代码提炼成函数
第一种情况是:同一个class内的两个函数含有相同表达式(expression)。
void printOwing(String _name) {
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(String _name) {
printBanner();
double outstanding = getOutstanding();
printDetails(_name,outstanding);
}
void printBanner() {
// print banner
System.out.println ("**************************");
System.out.println ("***** Customer Owes ******");
System.out.println ("**************************");
}
void printDetails (String _name,double outstanding) {
System.out.println ("name:" + _name);
System.out.println ("amount" + outstanding);
}
double getOutstanding() {
Enumeration e = _orders.elements();
double result = 0.0;
while (e.hasMoreElements()) {
Order each = (Order) e.nextElement();
result = each.getAmount();
}
return result;
}
第二种情况:两个subclasses有相同的表达式,或者是相似的表达式
优化的方法是:抽取相同的表达式(属性和方法),放在父类里,两个子类再去继承。
Pull Up Field/Method(字段上移动) (P32页)
两个子类拥有相同的字段,将该字段移至超类
如果是相似的表达式,好抽出共性的,则用模板函数设计模式来处理。
这里用到了面向对象的两个特性,继承和多态。
优化的思路Ⅰ
1、在各个subclass 中分解目标函数,把有差异的部分变成入参,封装成一个模板函数。
2、把模板函数放到父类中。
3、子类根据需要输入不同的入参,得到需要的结果。
\
如果是相似的表达式,差异的地方不好抽共性,则用模板函数设计模式来处理。
这里用到了面向对象的两个特性,继承和覆写(overrides)。
优化的思路Ⅱ
1、在各个subclass 中分解目标函数,使分解后的各个函数要不完全相同,要不完全不同。
2、父类有一个主函数包含完全相同的函数和完全不同的函数:相同的函数,抽到父类中,不相同的函数在父类中定义一个函数。
3、子类继承父类,然后覆写完全不同的函数,再调用主函数可得到期望的结果。
第二招 把过长的函数变小
百分之九十九的场合里,要把函数变小,只需使用Extract Method(第一招)。找到函数中适合集在一起的部分,将它们提炼出来形成一个新函数。
如果函数内有大量的参数和临时变量,它们会对你的函数提炼形成阻碍。这时就要用Replace Temp with Query来消除这些临时变量
Replace Temp with Query(以查询取代临时变量)
优化的思路
1、找出只被赋值一次的临时变量。
2、将该临时变量声明为final
3、编译:这可确保该临时变量的确只被赋值一次。
4、将临时变量等号右侧部分提炼到一个独立函数中;
5、首先将函数声明为private。日后你可能会发现有更多class需要使用 它,彼时你可再放松对它的保护。
6、编译,测试:确保提炼出来的函数无任何连带影响(副作用),结果不变;
7、把临时变量全替换成独立出来的函数;over!
例子:未优化代码
double getPrice() {
int basePrice = _quantity * _itemPrice;
double discountFactor;
if (basePrice > 1000) discountFactor = 0.95;
else discountFactor = 0.98;
return basePrice * discountFactor;
}
开始优化 1~3步骤
double getPrice() {
final int basePrice = _quantity * _itemPrice;
final double discountFactor;
if (basePrice > 1000) discountFactor = 0.95;
else discountFactor = 0.98;
return basePrice * discountFactor;
}
4~6步骤
double getPrice() {
final int basePrice = basePrice();
final double discountFactor;
if (basePrice > 1000) discountFactor = 0.95;
else discountFactor = 0.98;
return basePrice * discountFactor;
}
private int basePrice() {
return _quantity * _itemPrice;
}
7步骤
double getPrice() {
final double discountFactor;
if (basePrice() > 1000) discountFactor = 0.95;
else discountFactor = 0.98;
return basePrice() * discountFactor;
}
private int basePrice() {
return _quantity * _itemPrice;
}
搞定basePrice之后,再以类似办法提炼出一个discountFactor():
double getPrice() {
final double discountFactor = discountFactor();
return basePrice() * discountFactor;
}
private double discountFactor() {
if (basePrice() > 1000) return 0.95;
else return 0.98;
}
最后的效果
double getPrice() {
return basePrice() * discountFactor();
}
private double discountFactor() {
if (basePrice() > 1000) return 0.95;
else return 0.98;
}
private int basePrice() {
return _quantity * _itemPrice;
}
通过以上的优化,一个大函数,已经变成了多个小函数,重点是代码的可读性提高了,顺带的代码量变少。
\
第三招 参数对象化
当你看到一个函数的入参有四,五个,甚至更多时,且好几个函数都使用这组入参,这时就要用参数对象化来优化代码。这些函数可能隶属同一个class,也可能隶属不同的classes 。这样一组参数就是所谓的Date Clump (数据泥团)」。这时用一个对象封装这些参数,再用对象取代它们。
优化思路
1、入参有四,五个,甚至更多时,就要着手优化;
2、用一个新的class封装入参,并把这些参数设置为private严格保护起来,写这些参数的get方法和set方法。
3、原函数的入参变成这个新的class对象,函数里的参数用class对象对应的属性替换。
4、编译测试;
5、将原先的参数全部去除之后,观察有无适当函数可以运用Move Method 搬移到参数对象之中。
例子:未优化的代码,
@Autowired
private AddressService addressService;
public List inquireAddressListAccount( Integer pageNum ,Integer pageSize,String addressName,String mobile,String zipCode,String consignee){
return addressService.inquireAddressList(pageNum,pageSize,addressName,mobile,zipCode,consignee);
}
参数太多,看着杂乱
优化后:
@Autowired
private AddressService addressService;
public List inquireAddressListAccount( Integer pageNum ,Integer pageSize,InquireAddressListInput output){
return addressService.inquireAddressList(pageNum,pageSize,output);
}
public class InquireAddressListInput(){
private String addressName;
private String mobile;
private String zipCode;
private String consignee;
public String getConsignee() {
return consignee;
}
public void setConsignee(String consignee) {
this.consignee = consignee;
}
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
public String getZipCode() {
return zipCode;
}
public void setZipCode(String zipCode) {
this.zipCode = zipCode;
}
public String getAddressName() {
return addressName;
}
public void setAddressName(String addressName) {
this.addressName = addressName;
}
}
第四招 大招-提炼类和提炼子类
如果想利用单一class做太多事情,其内往往就会出现太多instance变量。一旦如此,Duplicated Code也就接踵而至了。
Extract Class 是Extract Subclass 之外的另一种选择,两者之间的抉择其实就是委托(delegation)和继承(inheritance)之间的抉择。
情况一:某个class做了应该由两个classes做的事。(Extract Class)
优化思路Ⅰ
1、明确每个class所负的责任,该做什么事情;
2、建立一个新class,用以表现从旧class中分离出来的责任;
3、建立「从旧class访问新class」的连接关系;
4、每次搬移后,编译、测试。
5、决定是否让新的class曝光。
例子:未优化的代码
class Person{
private String _name;
private String _officeAreaCode;
private String _officeNumber;
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;
}
}
优化1~2步骤
可以将「与电话号码相关」的行为分离到一个独立class中
class TelephoneNumber{
private String _number;
private String _areaCode;
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;
}
}
优化3步骤
class Person...
private String _name;
private TelephoneNumber _officeTelephone = new TelephoneNumber();
public String getName() {
return _name;
}
public String getTelephoneNumber(){
return _officeTelephone.getTelephoneNumber();
}
TelephoneNumber getOfficeTelephone() {
return _officeTelephone;
}
\
情况二:class 中的某些特性(features)只被某些(而非全部)实体(instances)用到。Extract Subclass(提炼子类)
优化思路Ⅱ
1、为source class 定义一个新的subclass
2、为这个新的subclass 提供构造函数。
简单的作法是:让subclass 构造函数接受与superclass 构造函数相同的参数,并通过super 调用superclass 构造函数;
3、找出调用superclass 构造函数的所有地点。如果它们需要的是新建的subclass , 令它们改而调用新构造函数。
如果subclass 构造函数需要的参数和superclass 构造函数的参数不同,可以使用Rename Method 修改其参数列。如果subclass 构造函数不需要superclass 构造函数的某些参数,可以使用Rename Method 将它们去除。
如果不再需要直接实体化(具现化,instantiated)superclass ,就将它声明为抽象类。
4、逐一使用Push Down Method 和 Push Down Field 将source class 的特性移到subclass 去。
5、每次下移之后,编译并测试。
例子:未优化代码
--用来决定当地修车厂的工作报价
class JobItem ...
public JobItem (int unitPrice, int quantity, boolean isLabor, Employee employee) {
_unitPrice = unitPrice;
_quantity = quantity;
_isLabor = isLabor;
_employee = employee;
}
public int getTotalPrice() {
return getUnitPrice() * _quantity;
}
public int getUnitPrice(){
return (_isLabor) ?
_employee.getRate():
_unitPrice;
}
public int getQuantity(){
return _quantity;
}
public Employee getEmployee() {
return _employee;
}
private int _unitPrice;
private int _quantity;
private Employee _employee;
private boolean _isLabor;
class Employee...
public Employee (int rate) {
_rate = rate;
}
public int getRate() {
return _rate;
}
private int _rate;
优化1步骤
class LaborItem extends JobItem {}
优化2步骤
class LaborItem extends JobItem {
public LaborItem (int unitPrice, int quantity, boolean isLabor, Employee employee) {
super (unitPrice, quantity, isLabor, employee);
}
}
这就足以让新的subclass 通过编译了。但是这个构造函数会造成混淆:某些参数是LaborItem 所需要的,另一些不是。稍后我再来解决这个问题。
\
优化3步骤
清理构造函数参数列
class JobItem...
protected JobItem (int unitPrice, int quantity, boolean isLabor, Employee employee) {
_unitPrice = unitPrice;
_quantity = quantity;
_isLabor = isLabor;
_employee = employee;
}
public JobItem (int unitPrice, int quantity) {
this (unitPrice, quantity, false, null)
}
外部调用应该使用新构造函数:
JobItem j2 = new JobItem (10, 15);
\
测试通过后,再使用 Rename Method 修改subclass 构造函数:
class LaborItem
public LaborItem (int quantity, Employee employee) {
super (0, quantity, true, employee);
}
可以将JobItem 的特性向下搬移。先从函数幵始,我先运用 Push Down Method 对付getEmployee() 函数:
class LaborItem extends JobItem {
public LaborItem (int unitPrice, int quantity, boolean isLabor, Employee employee) {
super (unitPrice, quantity, isLabor, employee);
}
public LaborItem (int quantity, Employee employee) {
super (0, quantity, true, employee);
}
public Employee getEmployee() {
return _employee;
}
}
//因为_employee 值域也将在稍后被下移到LaborItem ,所以我现在先将它声明为protected。
class JobItem...
protected Employee _employee;
将_employee 值域声明protected 之后,我可以再次清理构造函数,让_employee 只在「即将去达的subclass 中」被初始化:
class JobItem...
protected Employee _employee;
protected JobItem (int unitPrice, int quantity, boolean isLabor) {
_unitPrice = unitPrice;
_quantity = quantity;
_isLabor = isLabor;
}
class LaborItem ...
public LaborItem (int quantity, Employee employee) {
super (0, quantity, true);
_employee = employee;
}
下一个优化_isLabor 值域,_isLabor 在JobItem是值为false,在LaborItem值为true。
可以用多态常量函数。所谓「多态常量函数」会在不同的subclass 实现版本中返回不同的固定值
class JobItem...
protected boolean isLabor() {
return false;
}
class LaborItem...
protected boolean isLabor() {
return true;
}
就可以摆脱_isLabor 值域了
通过多态代替条件的方式,重构代码
class JobItem ...
public int getUnitPrice(){
return (isLabor()) ?
_employee.getRate():
_unitPrice;
}
将它重构为:
class JobItem...
public int getUnitPrice(){
return _unitPrice;
}
class LaborItem...
public int getUnitPrice(){
return _employee.getRate();
}
\
使用某项值域的函数全被下移至subclass 后,我就可以使用 Push Down Field 将值域也下移。
最后的结果就是:
public class JobItem {
protected JobItem (int unitPrice, int quantity) {
_unitPrice = unitPrice;
_quantity = quantity;
}
public int getTotalPrice() {
return getUnitPrice() * _quantity;
}
public int getUnitPrice(){
return _unitPrice;
}
public int getQuantity(){
return _quantity;
}
private int _unitPrice;
private int _quantity;
}
//
public class LaborItem extends JobItem {
private Employee _employee;
public LaborItem(int quantity, Employee employee) {
super(0, quantity);
_employee = employee;
}
public Employee getEmployee() {
return _employee;
}
public int getUnitPrice() {
return _employee.getRate();
}
}
//public class Employee {
public Employee(int rate) {
_rate = rate;
}
public int getRate() {
return _rate;
}
private int _rate;
}
\
一个class如果拥有太多代码,也适合使用Extract Class和Extract Subclass。 重构代码,直接把以上四招看情况用上。