# 重构改变既有代码设计

重构是对软件内部结构的一种调整，目的是在不改变软件可观察行为的前提下，提高其可理解性，降低其修改成本。

参考内容：

1. 《重构，改变既有代码设计》。

## 重构原则

### 重构的目的

1. 改进软件的设计，设计良好的软件代码少bug少，更容易修改
2. 让代码更容易理解
3. 帮助找到代码中的bug
4. 提高编程的速度

### 重构的时机

1. 三次法则：第一次做一件事，第二次重复的做一件事，第三次仍然需要重复做，那这时就需要重构了
2. 添加功能时：这段代码无法让我轻松地添加功能
3. 修补Bug时：当查询Bug的原因，代码逻辑很不清晰，无法快速定位问题时
4. 复审代码时：可以结对编程

### 不可重构的情况

1. 原来的代码太混乱，不如重新写一个简单
2. 项目到达最后期限（时间不足，容易发生意外情况）

### 重构的难题

1. 数据库的难题：
   1. 与代码紧密耦合
   2. 需要做数据迁移
   3. 不做数据迁移，可以在数据库与对象模型之前建立隔离层（感觉不好）
2. 修改接口
   1. 修改接口会影响到与接口相关的所有功能
   2. 可以增加一个新的接口（版本）直到所有调用方都修改完毕，再作废旧版本
   3. 不要过早的暴露接口，尽量修改接口的访问策略，让重构更容易
3. 通过重构手法完成的设计改动，这一步是重构的实操。
   1. 大体想象以下重构的情况
   2. 不用担心他不能覆盖客户所有的需求
   3. 如果没有简单地重构的办法，就需要在设计上投入更多的力气

## 代码坏味道

### Duplicated Code

重复代码:

1. 同一类或者函数抽出一个私有方法
2. 两个互为兄弟的子类提炼代码并 pull up method，甚至可以使用模板方法设计模式
3. 两个毫不相关的类出现，需要根据情况处理。有可能应该提取到第三个类中，有可能属于某个类，然后另一个类调用它

### Long Method

过长的函数:

1. 程序越长越难理解，因为必须转换上下文看看子程序做了什么，抽取多个小函数可以省去很多麻烦
2. 当一段代码需要加点注释描述功能的时候，就是拆分的好时候
3. 要以用途命名，而不是实现手法
4. 如果发生条件表达式、循环（循环内的代码），往往也需要提取

### Large Class

过大的类：

1. 一个类拥有的功能过多
2. 解决内部的重复代码、大过长函数
3. 类的字段过多，将同类型的字段提取到一个新的类中
4. 类的方法过多，从调用该类的调用方来看将类的功能划分为多个接口

### Long Parameter List

过长的参数列表：

1. 不要多传，只需传入足够的、函数需要的东西即可
2. 函数所处的宿主类中有的东西也不必传递

### Divergent Change

发散式变化：

1. 举例：如果增加了数据库，必须修改这三个函数，这时可以称为发散式变化
2. 针对外部发生的变化有相应的修改，应该只发生在一个类上：增加一个数据库类即可

> 比如公司的合同服务，如果增加新的合同，如果是共用一套接口代码，就需要在合同的所有相关方法上增加内容

### Shotgun Surgry

霰弹式修改：

1. 每次发生变化，都要在许多类上进行一些小修改，这样很难寻找这些需要修改的地方，并且很容易遗漏修改
2. 需要将需要修改的地方统一放在一个类中

> 比如魔法值，如果枚举值发生变化，使用魔法值的地方都要进行修改，是很难找到对应的要修改的地方的，正确的做法是使用枚举值替代，只需要修改枚举对象即可

### Feature Envy

依恋情节：

1. 对象技术的全部要点在于：将数据对数据的行为操作绑定在一起
2. 当某个类对数据的关注超过了对自己数据的关注，说明发生了依恋情节
3. 解决办法，将操作其他类数据过多的函数移动到另一个类中

### Data Clumps

数据泥团：

1. 当发现多个类中拥有相同的字段，多函数签名中有相同的参数，那么就需要将他们提炼到一个对象中

🤔：

1. controller层很多web接口的入参有相同的字段，这个也要提取到一个单独的类中吗
2. 提取到单独的类中的话，如果某个调用发生变化，是否需要建立一个新的类替换原有的类，因为没法修改原有的类

### Primitive Obsession

基本类型偏执

### Switch Statements

少用switch语句：

1. switch语句的问题在于重复，相同的switch语句可能散布在不同的位置
2. 如果看到switch语句，考虑使用多态替换

> 那使用if也是一样的吧？坐着的意思可能是多状态的统一管理

### Parallel Inheritance Hierarchies

平行继承体系：

1. 是Shotgun Surgry的一种
2. 如果你要为某个类增加一个子类，同样的也要为另一个类增加一个子类
3. 解决方法为让一个继承体系的实例引用另一个继承体系的实例

> todo ???

### Lazy Class

冗余类：

1. 类没有存在的意义就删除他，因为理解、维护这部分代码是需要精力的
2. 如果是几乎没用用的类，或者功能较少的类，可以使用内部类替换
3. 或者折叠继承体系，移除超类或者子类，将其剔除的一方的方法移动到另一个中

### Speculative Generality

夸夸其谈未来性：

1. 加入各种各样的钩子，企图处理一些特殊情况或者一些非必要的情况（😄说的就是我）
2. 如果所有装置都会用到，那就值得去做；如果用不到，就不值得
3. 如果你的抽象类没有什么可以做的，可以考虑剔除抽象类

### Temporary Field

令人迷惑的临时字段：

1. 某个变量是为了某种情况而设定，这样的代码很难理解
2. 可以将使用到临时变量的一串代码逻辑，封装到一个函数中，然后调用这个函数
3. 如果具有多个临时变量，且逻辑较为复杂，可以考虑抽象成一个类

### Message Chains

过度耦合的消息链：

1. 一个对象调用了另一个对象，另一个对象还调用了另一个对象

### Middle Man

中间人：

1. 有时候不一定需要委托对象，两个对象直接打交道更好

### Inappropriate Intimacy

狎(xia)昵关系：

1. 两个类过度亲密，存在耦合
2. 比如继承往往会造成这种情况，有时候依赖可能比继承更适合

### Alternative Classes with Different Interfaces

异曲同工的类：

1. 两个函数做一样的事情，但是拥有不同的签名

> 🤔 有可能是后来者不知道有这个函数，所以又新增了一个，系统复杂时会有这种情况

### Incomplete Library Class

不完美的类库：

1. 类库不具有未卜先知的能力，很多功能无法满足系统的需求
2. 如果想要修改类库的一两个函数：引入外加函数（提供一个新函数，包装原有的函数进行扩展）
3. 如果想要给类库添加一大堆的功能：引入本地扩展（包装原有的功能类，并包装他的所有方法）

🤔:

* 用切面不可以，切面改变不了函数的入参或者返回值

### Data Class

纯数据类，只包含读写，不含任何方法：

1. 因为不提供任何方法，那么调用方一定进行非常细琐的操作
2. 找到所有的调用方，将部分的调用行为放入到DataClass中

🤔：

* Vo类转换Entity的代码可以放到Vo中
* 枚举值的判断枚举值是否相同的代码，可以放入到枚举对象中
* DDD业务模型的充血模型

### Refused Bequrest

被拒绝的遗赠：

1. 子类不需要父类的某个方法
2. 说明类的继承体系设计有误，需要给子类建立一个兄弟类，将不要的方法交给兄弟类
3. 这条可以忽略，不是很严重的坏味道，有时候也需要这样的设计

### Comments

过多的注释：

1. 如果代码注释过多，你该考虑提取方法了
2. 如果提取方法之后，仍然需要过多注释，可能需要对方法重新命名
3. 写注释之前记得先重构，不是不推荐写注释哦！

## 构筑测试体系

1. 类应该包含他们自己的测试代码。
2. 编写测试代码的最佳时机应该在开始编码之前。当需要添加新的特性时，首先要添加测试代码

## 简化条件表达式

### Decompose Conditional

分解条件表达式：

```java
if (date.before(SUMMER_START) || date.after(SUMMER_END)) {
    charge = quantity * winterRate + winterServiceCharge;
} else {
    charge = quantity * summerRate;
}

// ↓

if (notSummer(date)) {
    charge = wrinterCharge(quantity);
} else {
    charge = summerCharge(quantity);
}
```

### Consolidate Conditional Expression

合并条件表达式：

```java
if (_seniority < 2) return 0;
if (_mothsDisabled > 12) return 0;
if (isPartTime) return 0;

// ↓

if (isNotEligableForDisablility()) return 0;

boolean isNotEligableForDisablility() {
    return _seniority < 2 || _mothsDisabled > 12 || isPartTime;
}
```

### Consolidate Duplicate Conditional Fragments

合并重复的代码片段：

```java
if (isSpecialDeal) {
    total = price * 0.95;
    send();
} else {
    total = price * 0.85;
    send();
}

// ↓

if (isPecialDeal) {
    total = price * 0.95;
} else {
    total = price * 0.85;
}
send();
```

### Remove Control Flag

移除控制标记（以break语句或者return语句取代控制标记）。

### Replace Nested Conditional With Guard Clauses

以卫语句取代嵌套表达式：

```java
double getPayAmount() {
    double result;
    if (_Dead) result = deadAmount;
    else {
        if (_isSeparated) result = sparatedAmount();
        else {
            if (_isRetired) result = retiredAmount();
            else result = normalPayAmount();
        }
    }
    return result;
}

// ↓

double getPayAmount() {
    if(_isDead) return deadAmount();
    if(_isSeparated) return sparatedAmount();
    if(_isRetired) return retiredAmount();
    return normalPayAmount();
}
```

### Replacement Conditional With Polymorphism

以多态取代条件表达式。

🤔：

1. 应该是限定业务处理逻辑较多的情况吧

### Introduce Null Object

引入Null对象(将null值替换为Null对象)：

```
Consumer
- AConsumer
- BConsumer
- NullConsumer
```

提供一个Nullable接口：

```java
public interface Nullable {
    boolean isNull();
}
```

通过`isNull()`方法替换原有的`intance == null`。

也可以为null对象设置默认值：

```java
class NullConsumer {
    public String getName() {
        return "游客";
    }
}

class AConsumer {
    public String getName() {
        return "A";
    }
}

class ConusmerFactory {
    Consumer getConusmer(Integer type) {
        if (type == 1) {
            return new AConsumer();
        } else if (type == 2) {
            return new BConsumer();
        } else {
            return new NullConsumer();
        }
    }
}

// 代码变化：
Consumer c = ConsumerFactory.getConsumer(type);
if (c != null) {
    return c.getName();
} else {
    return "游客";
}
// 变为
ConsumerFactory.getConsumer(type).getName();

```

🤔：

1. 页面展示某个枚举描述时，通常要通过code获取枚举对象，然后再获取对象的描述信息，这时候如果code不存在，返回的对象就是null，需要先判空再获取描述信息。可以为枚举增加一个Null值，不用判空也可以调用getMessage方法获得空描述。

### Introduce Assertion

引入断言：

```java
double getExpenseLimit() {
    // _expenseLimit 不可能为null
    return _expenseLimit.getVal()
}

// ↓

double getExpenseLimit() {
    Assert.isNotNull(_expenseLimit.getVal());
    // 如果断言失败，代表程序员犯了一个错误
    return _expenseLimit.getVal();
}
```

## 重新组织函数

### Extract Method

提炼函数：

```java
void printOwing() {
    printBanner();

    System.out.println("name:" + _name);
    System.out.println("amount:" + _amount);
}

// ↓

void printOwing() {
    printBanner();
    printDetail();
}

void printDetail() {
    System.out.println("name:" + _name);
    System.out.println("amount:" + _amount);
}
```

> 注意局部变量处理

### Inline Method

内联函数：

```java
int getRating() {
    return (moreThanFiveLateDiveries()) ? 2 : 1;
}

boolean moreThanFiveLateDiveries() {
    return _numberOfLateDeliver > 5;
}

// ↓

int getRating() {
    return _numberOfLateDeliver > 5 ? 2 : 1; 
}
```

使用场景：

1. 函数getRating与函数moreThanFiveLateDiveries的逻辑都比较简单，合并后仍然清晰易懂，这时可以考虑合并两个函数
2. 有一个大型函数，里面包含很多不合理的小函数，可以先使用内联将其合并大大函数中，然后再重新抽取小函数

### Inline Temp

内联临时变量：

```java
double basePrise = anOrder.basePrice();
return basePrise > 1000;

// ↓

return anOrder.basePrice() > 1000;
```

### Replace Temp with Query

以查询取代临时变量：

```java
double basePrice = _quantity * _itemPrice;
if(basePrice > 1000) {
    return basePrice * 0.95;
} else {
    return basePrice * 0.98;
}

// ↓

double basePrice() {
    return _quantity * _itemPrice;
}

if(basePrice() > 1000) {
    return basePrice * 0.95;
} else {
    return basePrice * 0.98;
}
```

### Introduce Explaining Variable

引入解释性变量：

```java
if (platform.toUpperCase().indexOf("MAC") > -1 && 
    browser.toUpperCase().indexOf("IE") > -1 && 
    wasIntialized() && resize > 0) {
        //... 
}

// ↓

final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1;
final boolean isIE = browser.toUpperCase().indexOf("IE") > -1;
final boolean wasResized = wasIntialized() && resize > 0;
if (isMacOs && isIE && wasResized) {
    // ...
}
```

### Split Temporary Variable

分解临时变量(不要共用临时变量)：

```java
double temp = 2 * (_width * _height);
System.out.println(temp);
temp = _width * _height;
System.out.println(temp);

// ↓

final double perimeter = 2 * (_width * _height);
System.out.println(perimeter);
final double area = _width * _height;
System.out.println(area);
```

### Remove Assignments to Paramters

移除对参数的赋值：

```java
int discount(int inputVal, int quantity, int yearToDate) {
    if (inputVal > 50) inputVal -= 2;
}

// ↓

int discount(int inputVal, int quantity, int yearToDate) {
    int result = inputVal;
    if (inputVal > 50) result -= 2;
}
```

### Replace Method with Method Object

以函数对象取代函数（将函数放入一个单独的类中，这样函数中的局部变量就变成了对象中的字段，这样可以在同一个对象中将这个大型函数分解为多个小型函数）：

```java
class Order {
    // 计算订单价格
    double price() {
        double primaryBasePrice;
        double secondaryBasePrice;
        double tertiaryBasePrice;
    }
}

// ↓

// 价格计算类
class PriceCalcuator {
    private double primaryBasePrice;
    private double secondaryBasePrice;
    private double tertiaryBasePrice;
    private Order order;
    
    public PriceCalcuator(Order order) {
        this.order = order;
    }

    public double complete() {
        // xxxx
    }
}

// 调用

class Order {
    // 计算订单价格
    double price() {
        new PriceCalcuator(this).complete();
    }
}
```

🤔：

1. 一定是复杂的、拥有多个临时变量的、过长的函数，不然已经封装为了函数，抽出来就没有意义了

### Substitute Algorithm

替换算法：如果有更简单的写法，或者算法，请使用更简单更清晰的。

## 重新组织数据

### Self Encapsulate Field

自封装字段：

```java
private int _low, _high;
boolean include(int args) {
    return args >= _low && args <= _high;
}

// ↓

private int _low, _high;
boolean include(int args) {
    return args >= getLow() && args <= getHigh();
}
int getLow() {return _low;}
int getHigh() {reutn _high;}
```

* 直接访问变量的好处在于：代码比较容易阅读
* 间接访问变量的好处则在于：通过统一的get方法比较容易改变对这个变量获取的途径

使用规则应该遵守：

1. 使用直接访问的形式
2. 如果直接访问的形式无法满足需求，考虑使用间接的形式

🤔：

1. 感觉直接的会好一点，特别是在业务逻辑层，对数据进行额外的处理会让代码变得复杂

### Replace Data Value with Object

以对象取代数据值：

```java
class Order {
    String consumer;
}

// ↓

class Order {
    Consumer consumer;
}

class Consumer {
    String name;
}
```

1. 开发初期，简单地数据项可以完成需求，随着需求逐渐变得复杂，简单地字符串已经无法满足，这时可以考虑将数据值封装为对象

### Change Value to Reference

将值对象改变为引用对象：

1. 实际上是说，两个对象关联，要使用对象引用关联，而不是对象中某个字段的值进行关联

### Change Reference to Value

将引用对象改变为值对象，和上一条恰恰相反：

1. 如果将值对象变为引用对象之后，代码将变得很难用，这时候需要考虑变为值对象
2. 使用值对象应该注意，他们的值是不可变的（如果要修改，只能更换新的值）

### Replace Array with Object

以对象取代数组：

```java
String[] row = new String[3];
row[0] = "zhangsan";
row[1] = "15";

// ↓

Person row = new Person();
row.setName("zhangsan");
row.setAge("15");
```

### Duplicate Observed Data

复制被监视的数据：比如此数据被观察者管理。被复制的数据如果修改，观察者将无法收到通知。

## 在对象之间搬移特性

在对象设计中，决定责任在哪儿是最重要的事情之一。一个类如果包含了太多责任，这个类将会变得臃肿不堪:

🤔：

1. 这地方是做代码重构中最重要的需要考虑到的部分，很多类的责任划分不是那么的明确
2. 很多概念在一开始的时候很简单，随着需求的增加，简单地类将会变得很复杂

### Move Method & Move Field

检查类中的所有字段以及函数，判断是否需要搬移

### Extract Class

提炼类：分解类所负的责任，检查并精简/合并每个类的接口

### Inline Class

将类内联化，将这个类的所有特性搬移到另一个类中，然后移除原类。

### Hide Delegate

隐藏委托关系，在服务类上建立客户所需要的所有函数，用以对客户隐藏委托关系。

### Remove Middle Man

移除中间人，让客户直接调用委托类。

### Introduce Foregin Method

引入外加函数，在客户类中建立一个函数，并以第一参数形式传入一个服务类实例。

### Introduce Local Extension

引入本地扩展，建立一个新类，让他包含额外函数。让这个扩展品成为原类的子类或者包装类。

## 简化函数调用

### Rename Method

重命名函数，如果函数名称无法揭示函数的作用。如果函数名称不好命名，千万不要将就。如果细粒度的函数命名不好，代码阅读性将会变得很差。

### Add Paramter & Remove Paramter

* 给函数添加参数，让该对象带进函数所需信息（非优先项目）。
* 给函数移除参数，过多的参数容易发生代码的坏味道。

## 处理概括关系

## 大型重构
