# 重構
[TOC=2,3]
或許你應該知道了,重構是怎樣的,你也知道重構能帶來什么。在我剛開始學重構和設計模式的時候,我需要去找一些好的示例,以便于我更好的學習。有時候不得不創造一些更好的場景,來實現這些功能。
有一天,我發現當我需要我一次又一次地重復講述某些內容,于是我就計劃著把這些應該掌握的技能放到Github上,也就有了[Artisan Stack](https://github.com/artisanstack) 計劃。
每個程序員都不可避免地是一個Coder,一個沒有掌握好技能的Coder,算不上是手工藝人,但是是手工人。
藝,需要有創造性的方法。
## 為什么重構?
> 為了更好的代碼。
在經歷了一年多的工作之后,我平時的主要工作就是修Bug。剛開始的時候覺得無聊,后來才發現修Bug需要更好的技術。有時候你可能要面對著一坨一坨的代碼,有時候你可能要花幾天的時間去閱讀代碼。而,你重寫那幾十代碼可能只會花上你不到一天的時間。但是如果你沒辦法理解當時為什么這么做,你的修改只會帶來更多的bug。修Bug,更多的是維護代碼。還是前人總結的那句話對:
> 寫代碼容易,讀代碼難。
假設我們寫這些代碼只要半天,而別人讀起來要一天。為什么不試著用一天的時候去寫這些代碼,讓別人花半天或者更少的時間來理解。
如果你的代碼已經上線,雖然是一坨坨的。但是不要輕易嘗試,`沒有測試的重構`。
從前端開始的原因在于,寫得一坨坨且最不容易測試的代碼都在前端。
讓我們來看看我們的第一個訓練,相當有挑戰性。
## 重構uMarkdown
代碼及setup請見github: [js-refactor](https://github.com/artisanstack/js-refactor)
### 代碼說明
`uMarkdown`是一個用于將Markdown轉化為HTML的庫。代碼看上去就像一個很典型的過程代碼:
~~~
/* code */
while ((stra = micromarkdown.regexobject.code.exec(str)) !== null) {
str = str.replace(stra[0], '<code>\n' + micromarkdown.htmlEncode(stra[1]).replace(/\n/gm, '<br/>').replace(/\ /gm, ' ') + '</code>\n');
}
/* headlines */
while ((stra = micromarkdown.regexobject.headline.exec(str)) !== null) {
count = stra[1].length;
str = str.replace(stra[0], '<h' + count + '>' + stra[2] + '</h' + count + '>' + '\n');
}
/* mail */
while ((stra = micromarkdown.regexobject.mail.exec(str)) !== null) {
str = str.replace(stra[0], '<a href="mailto:' + stra[1] + '">' + stra[1] + '</a>');
}
~~~
選這個做重構的開始,不僅僅是因為之前在寫[EchoesWorks](https://github.com/phodal/echoesworks)的時候進行了很多的重構。而且它更適合于,`重構到設計模式`的理論。讓我們在重構完之后,給作者進行pull request吧。
Markdown的解析過程,有點類似于`Pipe and Filters`模式(架構模式)。
Filter即我們在代碼中看到的正規表達式集:
~~~
regexobject: {
headline: /^(\#{1,6})([^\#\n]+)$/m,
code: /\s\`\`\`\n?([^`]+)\`\`\`/g
~~~
他會匹配對應的Markdown類型,隨后進行替換和處理。而``str```,就是管理口的輸入和輸出。
接著,我們就可以對其進行簡單的重構。
(ps: 推薦用WebStrom來做重構,自帶重構功能)
作為一個示例,我們先提出codeHandler方法,即將上面的
~~~
/* code */
while ((stra = micromarkdown.regexobject.code.exec(str)) !== null) {
str = str.replace(stra[0], '<code>\n' + micromarkdown.htmlEncode(stra[1]).replace(/\n/gm, '<br/>').replace(/\ /gm, ' ') + '</code>\n');
}
~~~
提取方法成
~~~
codeFilter: function (str, stra) {
return str.replace(stra[0], '<code>\n' + micromarkdown.htmlEncode(stra[1]).replace(/\n/gm, '<br/>').replace(/\ /gm, ' ') + '</code>\n');
},
~~~
while語句就成了
~~~
while ((stra = regexobject.code.exec(str)) !== null) {
str = this.codeFilter(str, stra);
}
~~~
然后,運行所有的測試。
~~~
grunt test
~~~
同理我們就可以`mail`、`headline`等方法進行重構。接著就會變成類似于下面的代碼,
~~~
/* code */
while ((execStr = regExpObject.code.exec(str)) !== null) {
str = codeHandler(str, execStr);
}
/* headlines */
while ((execStr = regExpObject.headline.exec(str)) !== null) {
str = headlineHandler(str, execStr);
}
/* lists */
while ((execStr = regExpObject.lists.exec(str)) !== null) {
str = listHandler(str, execStr);
}
/* tables */
while ((execStr = regExpObject.tables.exec(str)) !== null) {
str = tableHandler(str, execStr, strict);
}
~~~
然后你也看到了,上面有一堆重復的代碼,接著讓我們用JavaScript的`奇技浮巧`,即apply方法,把上面的重復代碼變成。
~~~
['code', 'headline', 'lists', 'tables', 'links', 'mail', 'url', 'smlinks', 'hr'].forEach(function (type) {
while ((stra = regexobject[type].exec(str)) !== null) {
str = that[(type + 'Handler')].apply(that, [stra, str, strict]);
}
});
~~~
進行測試,blabla,都是過的。
~~~
Markdown
? should parse h1~h3
? should parse link
? should special link
? should parse font style
? should parse code
? should parse ul list
? should parse ul table
? should return correctly class name
~~~
快來試試吧, [https://github.com/artisanstack/js-refactor](https://github.com/artisanstack/js-refactor)
是時候討論這個Refactor利器了,最初看到這個重構的過程是從ThoughtWorks鄭大曄校開始的,只是之前對于Java的另外一個編輯器Eclipse的壞感。。這些在目前已經不是很重要了,試試這個公司里面應用廣泛的編輯器。
## Interllij Idea重構
開發的流程大致就是這樣子的,測試先行算是推薦的。
~~~
編寫測試->功能代碼->修改測試->重構
~~~
上次在和buddy聊天的時候,才知道測試在功能簡單的時候是后行的,在功能復雜不知道怎么手手的時候是先行的。
開始之前請原諒我對于Java語言的一些無知,然后,看一下我寫的Main函數:
~~~
package com.phodal.learing;
public class Main {
public static void main(String[] args) {
int c=new Cal().add(1,2);
int d=new Cal2().sub(2,1);
System.out.println("Hello,s");
System.out.println(c);
System.out.println(d);
}
}
~~~
代碼寫得還好(自我感覺),先不管Cal和Cal2兩個類。大部分都能看懂,除了c,d不知道他們表達的是什么意思,于是。
### Rename
**快捷鍵:Shift+F6**
**作用:重命名**
- 把光標丟到int c中的c,按下shift+f6,輸入result_add
- 把光標移到int d中的d,按下shift+f6,輸入result_sub
于是就有
~~~
package com.phodal.learing;
public class Main {
public static void main(String[] args) {
int result_add=new Cal().add(1,2);
int result_sub=new Cal2().sub(2,1);
System.out.println("Hello,s");
System.out.println(result_add);
System.out.println(result_sub);
}
}
~~~
### Extract Method
**快捷鍵:alt+command+m**
**作用:擴展方法**
- 選中System.out.println(result_add);
- 按下alt+command+m
- 在彈出的窗口中輸入mprint
于是有了
~~~
public static void main(String[] args) {
int result_add=new Cal().add(1,2);
int result_sub=new Cal2().sub(2,1);
System.out.println("Hello,s");
mprint(result_add);
mprint(result_sub);
}
private static void mprint(int result_sub) {
System.out.println(result_sub);
}
~~~
似乎我們不應該這樣對待System.out.println,那么讓我們內聯回去
### Inline Method
**快捷鍵:alt+command+n**
**作用:內聯方法**
- 選中main中的mprint
- alt+command+n
- 選中Inline all invocations and remove the method(2 occurrences) 點確定
然后我們等于什么也沒有做了~~:
~~~
public static void main(String[] args) {
int result_add=new Cal().add(1,2);
int result_sub=new Cal2().sub(2,1);
System.out.println("Hello,s");
System.out.println(result_add);
System.out.println(result_sub);
}
~~~
似乎這個例子不是很好,但是夠用來說明了。
### Pull Members Up
開始之前讓我們先看看Cal2類:
~~~
public class Cal2 extends Cal {
public int sub(int a,int b){
return a-b;
}
}
~~~
以及Cal2的父類Cal
~~~
public class Cal {
public int add(int a,int b){
return a+b;
}
}
~~~
最后的結果,就是將Cal2類中的sub方法,提到父類:
~~~
public class Cal {
public int add(int a,int b){
return a+b;
}
public int sub(int a,int b){
return a-b;
}
}
~~~
而我們所要做的就是鼠標右鍵
### 重構之以查詢取代臨時變量
快捷鍵
Mac: 木有
Windows/Linux: 木有
或者: `Shift`+`alt`+`command`+`T` 再選擇 `Replace Temp with Query`
鼠標: **Refactor** | `Replace Temp with Query`
#### 重構之前
過多的臨時變量會讓我們寫出更長的函數,函數不應該太多,以便使功能單一。這也是重構的另外的目的所在,只有函數專注于其功能,才會更容易讀懂。
以書中的代碼為例
~~~
import java.lang.System;
public class replaceTemp {
public void count() {
double basePrice = _quantity * _itemPrice;
if (basePrice > 1000) {
return basePrice * 0.95;
} else {
return basePrice * 0.98;
}
}
}
~~~
#### 重構
選中`basePrice`很愉快地拿鼠標點上面的重構

Replace Temp With Query
便會返回
~~~
import java.lang.System;
public class replaceTemp {
public void count() {
if (basePrice() > 1000) {
return basePrice() * 0.95;
} else {
return basePrice() * 0.98;
}
}
private double basePrice() {
return _quantity * _itemPrice;
}
}
~~~
而實際上我們也可以
1.
選中
_quantity * _itemPrice
1.
對其進行`Extrace Method`
1.
選擇`basePrice`再`Inline Method`
#### Intellij IDEA重構
在Intellij IDEA的文檔中對此是這樣的例子
~~~
public class replaceTemp {
public void method() {
String str = "str";
String aString = returnString().concat(str);
System.out.println(aString);
}
}
~~~
接著我們選中`aString`,再打開重構菜單,或者
`Command`+`Alt`+`Shift`+`T` 再選中Replace Temp with Query
便會有下面的結果:
~~~
import java.lang.String;
public class replaceTemp {
public void method() {
String str = "str";
System.out.println(aString(str));
}
private String aString(String str) {
return returnString().concat(str);
}
}
~~~