重构:改善既有代码的设计之

384 阅读13分钟

本文已参与「新人创作礼」活动.一起开启掘金创作之路。

重构:改善既有代码的设计之(一)

本次分享目录

重构背景

何谓重构

为何重构

何时重构

代码坏的味道

Duplicated Code(重复的代码)
Long Method(过长函数)
Long Parameter List(过长参数列)
Large Class(过大类)

分享之一总结四招:

    一、重复的代码提炼成函数

    二、把过长的函数变小

    三、参数列太长或变化太频繁,参数对象化

    四、类的代码行数太多,要考虑提炼子类。

\

重构背景

**
**

不改变软件可观察行为的前提下改善其内部结构。---Martin Fowler
通俗来讲:看起来没做啥调整,让系统继续更好的满足客户需求。同时,让这个系统能够多蹦跶几年。 

系统已经在运行,不能停下。但是使用的轮子还是方型的轮子...性能效率都..

image.png

何谓重构

重构(名词)

对软件内部结构的一种调整,目的是在不改变"软件可观察行为"的前提下,提高其可理解性,降低其修改成本。简言之,在代码写好之后改进它的设计。

重构(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);
 
  }
 

实际上这三部分都可以提炼。 image.png 优化后的结果

 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页)

两个子类拥有相同的字段,将该字段移至超类

image.png

如果是相似的表达式,好抽出共性的,则用模板函数设计模式来处理。

这里用到了面向对象的两个特性,继承和多态。

image.png

优化的思路

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。 重构代码,直接把以上四招看情况用上。