# 重構篇
什么是重構?
> 重構,一言以蔽之,就是在不改變外部行為的前提下,有條不紊地改善代碼。
相似的
> 代碼重構(英語:Code refactoring)指對軟件代碼做任何更動以增加可讀性或者簡化結構而不影響輸出結果。
### 網站重構
與上述相似的是:在不改變外部行為的前提下,簡化結構、添加可讀性,而在網站前端保持一致的行為。也就是說是在不改變UI的情況下,對網站進行優化,在擴展的同時保持一致的UI。
### 基礎網站重構
過去人們所說的`網站重構`
> 把“未采用CSS,大量使用HTML進行定位、布局,或者雖然已經采用CSS,但是未遵循HTML結構化標準的站點”變成“讓標記回歸標記的原本意義。通過在HTML文檔中使用結構化的標記以及用CSS控制頁面表現,使頁面的實際內容與它們呈現的格式相分離的站點。”的過程就是網站重構(Website Reconstruction)
依照我做過的一些案例,對于傳統的網站來說重構通常是
- 表格(table)布局改為DIV+CSS
- 使網站前端兼容于現代瀏覽器(針對于不合規范的CSS、如對IE6有效的)
- 對于移動平臺的優化
- 針對于SEO進行優化
### 高級網站重構
過去的網站重構就是“DIV+CSS”,想法固然極度局限。但也不是另一部分的人認為是“XHTML+CSS”,因為“XHTML+CSS”只是頁面重構。
而真正的網站重構
> 應包含結構、行為、表現三層次的分離以及優化,行內分工優化,以及以技術與數據、人文為主導的交互優化等。
深層次的網站重構應該考慮的方面
- 減少代碼間的耦合
- 讓代碼保持彈性
- 嚴格按規范編寫代碼
- 設計可擴展的API
- 代替舊有的框架、語言(如VB)
- 增強用戶體驗
通常來說對于速度的優化也包含在重構中
- 壓縮JS、CSS、image等前端資源(通常是由服務器來解決)
- 程序的性能優化(如數據讀寫)
- 采用CDN來加速資源加載
- 對于JS DOM的優化
- HTTP服務器的文件緩存
可以應用的的方面
- [使用Ngx_pagespeed優化前端](http://www.phodal.com/blog/nginx-with-ngx-pagespeed-module-improve-website-cache/)
- 解耦復雜的模塊
- 對緩存進行優化
- 針對于內容創建或預留API
- 需要添加新API,如(weChat等的支持)
- 用新的語言、框架代碼舊的框架(如VB.NET,C#.NET)
### 網站重構目的
希望自己的網站
- 成本變得更低
- 運行得更好
- 訪問者更多
- 維護愈加簡單
- 功能更強
### 代碼重構——為了更好的代碼。
在經歷了一年多的工作之后,我平時的主要工作就是修Bug。剛開始的時候覺得無聊,后來才發現修Bug需要更好的技術。有時候你可能要面對著一坨一坨的代碼,有時候你可能要花幾天的時間去閱讀代碼。而,你重寫那幾十代碼可能只會花上你不到一天的時間。但是如果你沒辦法理解當時為什么這么做,你的修改只會帶來更多的bug。修Bug,更多的是維護代碼。還是前人總結的那句話對:
> 寫代碼容易,讀代碼難。
### 使用工具重構
### 重構之提煉函數
Intellij IDEA帶了一些有意思的快捷鍵,或者說自己之前不在意這些快捷鍵的存在。重構作為單獨的一個菜單,顯然也突顯了其功能的重要性,說說**提煉函數**,或者說提出方法。
快捷鍵
Mac: `alt`+`command`+`M`
Windows/Linux: `Ctrl`+`Alt`+`M`
鼠標: Refactor | Extract | Method
#### 重構之前
以重構一書代碼為例,重構之前的代碼
~~~
public class extract {
private String _name;
void printOwing(double amount){
printBanner();
System.out.println("name:" + _name);
System.out.println("amount" + amount);
}
private void printBanner() {
}
}
~~~
#### 重構
選中
~~~
System.out.println("name:" + _name);
System.out.println("amount" + amount);
~~~
按下上述的快捷鍵,會彈出下面的對話框

Extrct Method
輸入
~~~
printDetails
~~~
那么重構就完成了。
#### 重構之后
IDE就可以將方法提出來
~~~
public class extract {
private String _name;
void printOwing(double amount){
printBanner();
printDetails(amount);
}
private void printDetails(double amount) {
System.out.println("name:" + _name);
System.out.println("amount" + amount);
}
private void printBanner() {
}
}
~~~
#### 重構
還有一種就以Intellij IDEA的示例為例,這像是在說其的智能。
~~~
public class extract {
public void method() {
int one = 1;
int two = 2;
int three = one + two;
int four = one + three;
}
}
~~~
只是這次要選中的只有一行,
``int three = one + two;`
以便于其的智能,它便很愉快地告訴你它又找到了一個重復
~~~
IDE has detected 1 code fragments in this file that can be replaced with a call to extracted method...
~~~
便返回了這樣一個結果
~~~
public class extract {
public void method() {
int one = 1;
int two = 2;
int three = add(one, two);
int four = add(one, three);
}
private int add(int one, int two) {
return one + two;
}
}
~~~
然而我們就可以很愉快地繼續和它玩耍了。當然這其中還會有一些更復雜的情形,當學會了這一個剩下的也不難了。
### 重構之內聯函數
繼續走這重構一書的復習之路,接著便是內聯,除了內聯變量,當然還有內聯函數。
快捷鍵
Mac: `alt`+`command`+`M`
Windows/Linux: `Ctrl`+`Alt`+`M`
鼠標: Refactor | Inline
#### 重構之前
以之前的[提煉函數](http://www.phodal.com/blog/intellij-idea-refactor-extract-method/)為例
~~~
public class extract {
public void method() {
int one = 1;
int two = 2;
int three = add(one, two);
int four = add(one, three);
}
private int add(int one, int two) {
return one + two;
}
}
~~~
在`add(one,two)`很愉快地按上個快捷鍵吧,就會彈出

Inline Method
再輕輕地回車,Refactor就這么結束了。。
#### Intellij Idea 內聯臨時變量
以書中的代碼為例
~~~
double basePrice = anOrder.basePrice();
return (basePrice > 1000);
~~~
同樣的,按下`Command`+`alt`+`N`
~~~
return (anOrder.basePrice() > 1000);
~~~
對于python之類的語言也是如此
~~~
def inline_method():
baseprice = anOrder.basePrice()
return baseprice > 1000
~~~
### 重構之以查詢取代臨時變量
繼續看看有木有什么木有試過的功能,從之前的[內聯函數](http://www.phodal.com/blog/intellij-idea-refactor-inline-method/)、[提煉函數](http://www.phodal.com/blog/intellij-idea-refactor-extract-method/)再到現在的[Replace Temp With Query](http://www.phodal.com/blog/intellij-idea-refactor-replace-temp-with-query)(以查詢取代臨時變量)
快捷鍵
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的文檔中對此是這樣的例子
~~~
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);
}
}
~~~
### 借助工具重構
- 當你寫了一大堆代碼,你沒有意識到里面有一大堆重復。
- 當你寫了一大堆測試,卻不知道覆蓋率有多少。
這就是個問題了,于是偶然間看到了一個叫code climate的網站。
### Code Climate
> Code Climate consolidates the results from a suite of static analysis tools into a single, real-time report, giving your team the information it needs to identify hotspots, evaluate new approaches, and improve code quality.
Code Climate整合一組靜態分析工具的結果到一個單一的,實時的報告,讓您的團隊需要識別熱點,探討新的方法,提高代碼質量的信息。
簡單地來說:
- 對我們的代碼評分
- 找出代碼中的壞味道
于是,我們先來了個例子
| Rating | Name | Complexity | Duplication | Churn | C/M | Coverage | Smells |
|-----|-----|-----|-----|-----|-----|-----|-----|
| A | lib/coap/coap_request_handler.js | 24 | 0 | 6 | 2.6 | 46.4% | 0 |
| A | lib/coap/coap_result_helper.js | 14 | 0 | 2 | 3.4 | 80.0% | 0 |
| A | lib/coap/coap_server.js | 16 | 0 | 5 | 5.2 | 44.0% | 0 |
| A | lib/database/db_factory.js | 8 | 0 | 3 | 3.8 | 92.3% | 0 |
| A | lib/database/iot_db.js | 7 | 0 | 6 | 1.0 | 58.8% | 0 |
| A | lib/database/mongodb_helper.js | 63 | 0 | 11 | 4.5 | 35.0% | 0 |
| C | lib/database/sqlite_helper.js | 32 | 86 | 10 | 4.5 | 35.0% | 2 |
| B | lib/rest/rest_helper.js | 19 | 62 | 3 | 4.7 | 37.5% | 2 |
| A | lib/rest/rest_server.js | 17 | 0 | 2 | 8.6 | 88.9% | 0 |
| A | lib/url_handler.js | 9 | 0 | 5 | 2.2 | 94.1% | 0 |
分享得到的最后的結果是:

Coverage
#### 代碼的壞味道
于是我們就打開`lib/database/sqlite_helper.js`,因為其中有兩個壞味道
> Similar code found in two :expression_statement nodes (mass = 86)
在代碼的 `lib/database/sqlite_helper.js:58…61 < >`
~~~
SQLiteHelper.prototype.deleteData = function (url, callback) {
'use strict';
var sql_command = "DELETE FROM " + config.table_name + " where " + URLHandler.getKeyFromURL(url) + "=" + URLHandler.getValueFromURL(url);
SQLiteHelper.prototype.basic(sql_command, callback);
~~~
lib/database/sqlite_helper.js:64…67 < >
與
~~~
SQLiteHelper.prototype.getData = function (url, callback) {
'use strict';
var sql_command = "SELECT * FROM " + config.table_name + " where " + URLHandler.getKeyFromURL(url) + "=" + URLHandler.getValueFromURL(url);
SQLiteHelper.prototype.basic(sql_command, callback);
~~~
只是這是之前修改過的重復。。
原來的代碼是這樣的
~~~
SQLiteHelper.prototype.postData = function (block, callback) {
'use strict';
var db = new sqlite3.Database(config.db_name);
var str = this.parseData(config.keys);
var string = this.parseData(block);
var sql_command = "insert or replace into " + config.table_name + " (" + str + ") VALUES (" + string + ");";
db.all(sql_command, function (err) {
SQLiteHelper.prototype.errorHandler(err);
db.close();
callback();
});
};
SQLiteHelper.prototype.deleteData = function (url, callback) {
'use strict';
var db = new sqlite3.Database(config.db_name);
var sql_command = "DELETE FROM " + config.table_name + " where " + URLHandler.getKeyFromURL(url) + "=" + URLHandler.getValueFromURL(url);
db.all(sql_command, function (err) {
SQLiteHelper.prototype.errorHandler(err);
db.close();
callback();
});
};
SQLiteHelper.prototype.getData = function (url, callback) {
'use strict';
var db = new sqlite3.Database(config.db_name);
var sql_command = "SELECT * FROM " + config.table_name + " where " + URLHandler.getKeyFromURL(url) + "=" + URLHandler.getValueFromURL(url);
db.all(sql_command, function (err, rows) {
SQLiteHelper.prototype.errorHandler(err);
db.close();
callback(JSON.stringify(rows));
});
};
~~~
說的也是大量的重復,重構完的代碼
~~~
SQLiteHelper.prototype.basic = function(sql, db_callback){
'use strict';
var db = new sqlite3.Database(config.db_name);
db.all(sql, function (err, rows) {
SQLiteHelper.prototype.errorHandler(err);
db.close();
db_callback(JSON.stringify(rows));
});
};
SQLiteHelper.prototype.postData = function (block, callback) {
'use strict';
var str = this.parseData(config.keys);
var string = this.parseData(block);
var sql_command = "insert or replace into " + config.table_name + " (" + str + ") VALUES (" + string + ");";
SQLiteHelper.prototype.basic(sql_command, callback);
};
SQLiteHelper.prototype.deleteData = function (url, callback) {
'use strict';
var sql_command = "DELETE FROM " + config.table_name + " where " + URLHandler.getKeyFromURL(url) + "=" + URLHandler.getValueFromURL(url);
SQLiteHelper.prototype.basic(sql_command, callback);
};
SQLiteHelper.prototype.getData = function (url, callback) {
'use strict';
var sql_command = "SELECT * FROM " + config.table_name + " where " + URLHandler.getKeyFromURL(url) + "=" + URLHandler.getValueFromURL(url);
SQLiteHelper.prototype.basic(sql_command, callback);
};
~~~
重構完后的代碼比原來還長,這似乎是個問題~~
### 一次測試驅動開發
### 故事
之前正在重寫一個[物聯網](http://www.phodal.com/iot)的服務端,主要便是結合CoAP、MQTT、HTTP等協議構成一個物聯網的云服務。現在,主要的任務是集中于協議與授權。由于,不同協議間的授權是不一樣的,最開始的時候我先寫了一個http put授權的功能,而在起先的時候是如何測試的呢?
~~~
curl --user root:root -X PUT -d '{ "dream": 1 }' -H "Content-Type: application/json" http://localhost:8899/topics/test
~~~
我只要順利在request中看有無`req.headers.authorization`,我便可以繼續往下,接著給個判斷。畢竟,我們對HTTP協議還是蠻清楚的。
~~~
if (!req.headers.authorization) {
res.statusCode = 401;
res.setHeader('WWW-Authenticate', 'Basic realm="Secure Area"');
return res.end('Unauthorized');
}
~~~
可是除了HTTP協議,還有MQTT和CoAP。對于MQTT協議來說,那還算好,畢竟自帶授權,如:
~~~
mosquitto_pub -u root -P root -h localhost -d -t lettuce -m "Hello, MQTT. This is my first message."
~~~
便可以讓我們簡單地完成這個功能,然而有的協議是沒有這樣的功能如CoAP協議中是用Option來進行授權的。現在的工具如libcoap只能有如下的簡單功能
~~~
coap-client -m get coap://127.0.0.1:5683/topics/zero -T
~~~
于是,先寫了個測試腳本來驗證功能。
~~~
var coap = require('coap');
var request = coap.request;
var req = request({hostname: 'localhost',port:5683,pathname: '',method: 'POST'});
...
req.setHeader("Accept", "application/json");
req.setOption('Block2', [new Buffer('phodal'), new Buffer('phodal')]);
...
req.end();
~~~
寫完測試腳本后發現不對了,這個不應該是測試的代碼嗎? 于是將其放到了spec中,接著發現了上面的全部功能的實現過程為什么不用TDD實現呢?
### 說說測試驅動開發
測試驅動開發是一個很“古老”的程序開發方法,然而由于國內的開發流程的問題——即開發人員負責功能的測試,導致這么好的一項技術沒有在國內推廣。
測試驅動開發的主要過程是:
1. 先寫功能的測試
1. 實現功能代碼
1. 提交代碼(commit -> 保證功能正常)
1. 重構功能代碼
而對于這樣的一個物聯網項目來說,我已經有了幾個有利的前提:
1. 已經有了原型
1. 框架設計
### 思考
通常在我的理解下,TDD是可有可無的。既然我知道了我要實現的大部分功能,而且我也知道如何實現。與此同時,對Code Smell也保持著警惕、要保證功能被測試覆蓋。那么,總的來說TDD帶來的價值并不大。
然而,在當前這種情況下,我知道我想要的功能,但是我并不理解其深層次的功能。我需要花費大量的時候來理解,它為什么是這樣的,需要先有一些腳本來知道它是怎么工作的。TDD變顯得很有價值,換句話來說,在現有的情況下,TDD對于我們不了解的一些事情,可以驅動出更多的開發。畢竟在我們完成測試腳本之后,我們也會發現這些測試腳本成為了代碼的一部分。
在這種理想的情況下,我們為什么不TDD呢?