[TOC]
在我們真正開始去寫代碼之前,我們可能會去考慮一些事情。怎么去規劃我們的任務,如何去細分這個任務。
1. 如果一件事可以自動化,那么就盡量去自動化,畢竟你是一個程序員。
2. 快捷鍵!快捷鍵!快捷鍵!
3. 使用可以幫助你快速工作的工具——如啟動器。
不過不得不提到的一點是:你需要去考慮這個需求是不是一個坑的問題。如果這是個一個坑,那么你應該盡早的去反饋這個問題。溝通越早,成本越低。
## 編碼過程
整個編程的過程如下圖所示:

編碼過程
步驟如下所示:
1. Kick Off。在這個步驟中,我們要詳細地了解我們所需要做的東西、我們的驗收條件是什么、我們需要做哪些事情。
2. Tasking。**簡單**的規則一下,我們需要怎么做。一般來說,如果是結對編程的話,還會記錄下來。
3. 最新的代碼。對于使用 Git 來管理項目的團隊來說,在一個任務剛開始的時候應該保證本地的代碼是最新的。
4. Test First。測試優先是一個很不錯的實踐,可以保證我們寫的代碼的健壯,并且函數盡可能小,當然也會有測試。
5. Code。就是實現功能,一般人都知道。
6. 重構。在我們實現了上面兩步之后,我們還需要重構代碼,使我們的代碼更容易閱讀、更易懂等等。
7. 提交代碼。這里的提交代碼只是本地的提交代碼,因此都提倡在本地多次提交代碼。
8. 運行測試。當我們完成我們的任務后,我們就可以準備 PUSH 代碼了。在這時,我們需要在本地運行測試——以保證我們不破壞別人的功能。
9. PUSH 代碼。
10. 等 CI 測試通過。如果這時候 CI 是掛的話,那么我們就需要再修 CI。這時其他的人就沒有理由 PUSH 代碼,如果他們的代碼也是有問題的,這只會使情況變得愈加復雜。
不過,在最開始的時候我們要了解一下如何去搭建一個項目。
## Web 應用的構建系統
> 構建系統(build system)是用來從源代碼生成用戶可以使用的目標的自動化工具。目標可以包括庫、可執行文件、或者生成的腳本等等。
常用的構建工具包括 GNU Make、GNU autotools、CMake、Apache Ant(主要用于JAVA)。此外,所有的集成開發環境(IDE)比如 Qt Creator、Microsoft Visual Studio 和 Eclipse 都對他們支持的語言添加了自己的構建系統配置工具。通常 IDE 中的構建系統只是基于控制臺的構建系統(比如 Autotool 和 CMake )的前端。
對比于 Web 應用開發來說,構建系統應該還包括應用打包(如 Java 中的 Jar 包,或者用于部署的 RPM 包)、源代碼分析、測試覆蓋率分析等等。
### Web 應用的構建過程
在剛創建項目的時候,我們都會有一個完整的構建思路。如下圖便是這樣的一個例子:

構建過程
這是一個后臺語言用的是 Java,前臺語言用的是 JavaScript 項目的構建流程。
**Compile**。對于那些不是用瀏覽器的前端項目來說,如 ES6、CoffeeScript,他們還需要將代碼編譯成瀏覽器使用的 JavaScript 版本。對于 Java 語言來說,他需要一個編譯的過程,在這個編譯的過程中,會檢查一些語法問題。
**Check Style**。通常我們會在我們的項目里定義一些代碼規范,如 JavaScript 中的使用兩個空格的縮進,Java 的 Checkstyle 中一個函數不能超過30行的限制。
**單元測試**。作為測試中最基礎也是最快的測試,這個測試將集中于測試單個函數的是不是正確的。
**功能測試**。功能測試的意義在于,保證一個功能依賴的幾個函數組合在一起也是可以工作的。
**Mock Server**。當我們的代碼依賴于第三方服務的時候,我們就需要一個 Mock Server 來保證我們的功能代碼可以獨立地測試。
**集成測試**。這一步將集成前臺、后臺,并且運行起最后將上線的應用。接著依據于用戶所需要的功能來編寫相應的測試,來保證一個個的功能是可以工作的。
**打包**。對于部署來說,直接安裝一個 RPM 包,或者 DEB 包是最方便的事。在這個包里會包含應用程序所需的所有二進制文件、數據和配置文件等等。
**上傳包**。在完成打包后,我們就可以上傳這個軟件包了。
**部署**。最后,我們就可以在我們的線上環境中安裝這個軟件包。
### Web 應用的構建實戰
下面就讓我們來構建一個簡單的 Web 應用,來實踐一下這個過程。在這里,我們要使用到的一個工具是 Gulp,當然對于 Grunt 也是類似的。
#### Gulp 入門指南
> Gulp.js 是一個自動化構建工具,開發者可以使用它在項目開發過程中自動執行常見任務。Gulp.js 是基于 Node.js 構建的,利用 Node.js 流的威力,你可以快速構建項目并減少頻繁的 IO 操作。Gulp.js 源文件和你用來定義任務的 Gulp 文件都是通過 JavaScript(或者 CoffeeScript )源碼來實現的。
1. 全局安裝 gulp:
~~~
$ npm install --global gulp
~~~
1. 作為項目的開發依賴(devDependencies)安裝:
~~~
$ npm install --save-dev gulp
~~~
1. 在項目根目錄下創建一個名為 gulpfile.js 的文件:
~~~
var gulp = require('gulp');
gulp.task('default', function() {
// 將你的默認的任務代碼放在這
});
~~~
1. 運行 gulp:
~~~
$ gulp
~~~
默認的名為 default 的任務(task)將會被運行,在這里,這個任務并未做任何事情。接下來,我們就可以打造我們的應用的構建系統了。
#### 代碼質量檢測工具
當 C 還是一門新型的編程語言時,還存在一些未被原始編譯器捕獲的常見錯誤,所以程序員們開發了一個被稱作 lint 的配套項目用來掃描源文件,查找問題。
對應于不同的語言都會有不同的 lint 工具,在 JavaScript 中就有 JSLint。JavaScript 是一門年輕、語法靈活多變且對格式要求相對松散的語言,因此這樣的工具對于這門語言來說比較重要。
2011年,一個叫 Anton Kovalyov 的前端程序員借助開源社區的力量弄出來了 JSHint,其思想基本上和 JSLint 是一致的,但是其有一下幾項優勢:
* 可配置規則,每個團隊可以自己定義自己想要的代碼規范。
* 對社區非常友好,社區支持度高。
* 可定制的結果報表。
下面就讓我們來安裝這個軟件吧:
**安裝及使用**
~~~
npm install jshint gulp-jshint --save-dev
~~~
示例代碼:
~~~
var jshint = require('gulp-jshint');
var gulp = require('gulp');
gulp.task('lint', function() {
return gulp.src('./lib/*.js')
.pipe(jshint())
.pipe(jshint.reporter('YOUR_REPORTER_HERE'));
});
~~~
#### 自動化測試工具
一般來說,自動測試應該從兩部分考慮:
* 單元測試
* 功能測試
Mocha 是一個可以運行在 Node.js 和瀏覽器環境里的測試框架,
~~~
var gulp = require('gulp');
var mocha = require('gulp-mocha');
gulp.task('default', function () {
return gulp.src('test.js', {read: false})
// gulp-mocha needs filepaths so you can't have any plugins before it
.pipe(mocha({reporter: 'nyan'}));
});
~~~
#### 編譯
對于靜態型語言來說,編譯是一個很重要的步驟。不過,對于動態語言來說也存在這樣的工具。
**動態語言的編譯**
可以說這類型的語言,是以我們常見的 JavaScript 為代表。
1. CoffeeScript 是一套 JavaScript 的轉譯語言,并且它增強了 JavaScript 的簡潔性與可讀性。
2. Webpack 是一款模塊加載器兼打包工具,它能把各種資源,例如 JS(含JSX)、coffee、樣式(含less/sass)、圖片等都作為模塊來使用和處理。
3. Babel 是一個轉換編譯器,它能將 ES6 轉換成ES5,以便在較低版本的瀏覽器中正確運行。
#### 打包
在 GNU/Linux 系統的軟件包里通過包含了已壓縮的軟件文件集以及該軟件的內容信息。常見的軟件包有
1. DEB。Debian 軟件包格式,文件擴展名為 .deb
2. RPM(原 Red Hat Package Manager,現在是一個遞歸縮寫)。該軟件包分為二進制包(Binary)、源代碼包(Source)和 Delta 包三種。二進制包可以直接安裝在計算機中,而源代碼包將會由 RPM 自動編譯、安裝。源代碼包經常以 src.rpm 作為后綴名。
3. 壓縮文檔 tar.gz。通常是該軟件的源碼,故而在安裝的過程中需要編譯、安裝,并且在編譯時需要自己手動安裝所需要依賴的軟件。在軟件倉庫沒有最新版本的軟件時,tar.gz 往往是最好的選擇。
由于這里的打包過程比較繁瑣,就不介紹了。有興趣的讀者可以自己了解一下。
#### 上傳及發布包
上傳包之前我們需要創建一個相應的文件服務器,又或者是相應的軟件源。并且對于我們的產品環境的服務器來說,我們還需要指定好這個軟件源才能安裝這個包。
以 Ubuntu 為例,Ubuntu 里的許多應用程序軟件包,是放在網絡里的服務器上,這些服務器網站,就稱作“源”,從源里可以很方便地獲取軟件包。
因而在這一步中,我們所需要做的事便是將我們打包完的軟件上傳到相應的服務器上。
## Git 與版本控制
### 版本控制
> 版本控制是一種記錄一個或若干文件內容變化,以便將來查閱特定版本修訂情況的系統。
雖然基于 Git 的工作流可能并不是一個非常好的實踐,但是在這里我們以這個工作流做為實踐來開展我們的項目。如下圖所示是一個基于 Git 的項目流:

基于 Git 的工作流
我們日常會工作在 “develop” 分支(那條線)上,通常來說每個迭代我們會發布一個新的版本,而這個新的版本將會直接上線到產品環境。那么上線到產品環境的這個版本就需要打一個版本號——這樣不僅可以方便跟蹤我們的系統,而且當出錯的時候我們也可以直接回滾到上一個版本。如果在上線的時候有些 Bug 不得不去修復,并且由于上線的新功能很重要,我們就需要一些 Hotfix。而從整個過程來看,版本控制起了一個非常大的作用。
不僅僅如此,版本控制的最大重要是在開發的過程中扮演的角色。通過版本管理系統,我們可以:
1. 將某個文件回溯到之前的狀態。
2. 將項目回退到過去某個時間點。
3. 在修改 Bug 時,可以查看修改歷史,查出修改原因
4. 只要版本控制系統還在,你可以任意修改項目中的文件,并且還可以輕松恢復。
常用的版本管理系統有 Git、SVN,但是從近年來看 Git 似乎更受市場歡迎。
### Git
從一般開發者的角度來看,Git 有以下功能:
1. 從服務器上克隆數據庫(包括代碼和版本信息)到單機上。
2. 在自己的機器上創建分支,修改代碼。
3. 在單機上自己創建的分支上提交代碼。
4. 在單機上合并分支。
5. 新建一個分支,把服務器上最新版的代碼 fetch 下來,然后跟自己的主分支合并。
6. 生成補丁(patch),把補丁發送給主開發者。
7. 看主開發者的反饋,如果主開發者發現兩個一般開發者之間有沖突(他們之間可以合作解決的沖突),就會要求他們先解決沖突,然后再由其中一個人提交。如果主開發者可以自己解決,或者沒有沖突,就通過。
8. 一般開發者之間解決沖突的方法,開發者之間可以使用 pull 命令解決沖突,解決完沖突之后再向主開發者提交補丁。
從主開發者的角度(假設主開發者不用開發代碼)看,Git 有以下功能:
1. 查看郵件或者通過其它方式查看一般開發者的提交狀態。
2. 打上補丁,解決沖突(可以自己解決,也可以要求開發者之間解決以后再重新提交,如果是開源項目,還要決定哪些補丁有用,哪些不用)。
3. 向公共服務器提交結果,然后通知所有開發人員。
#### Git 初入
如果是第一次使用 Git,你需要設置署名和郵箱:
~~~
$ git config --global user.name "用戶名"
$ git config --global user.email "電子郵箱"
~~~
你可以在?[GitHub](https://github.com/)?新建免費的公開倉庫或在?[Coding.net](https://coding.net/)?新建免費的私有倉庫。
按照?[GitHub 的文檔](https://help.github.com/articles/generating-an-ssh-key/)?或?[Coding.net 的文檔](https://coding.net/help/doc/account/ssh-key.html)?配置 SSH Key,然后將代碼倉庫 clone 到本地,其實就是將代碼復制到你的機器里,并交由 Git 來管理:
~~~
$ git clone git@github.com:username/repository.git
或
$ git clone git@git.coding.net:username/repository.git
~~~
或使用 HTTPS 地址進行 clone:
~~~
$ git clone https://username:password@github.com/username/repository.git
或
$ git clone https://username:password@git.coding.net/username/repository.git
~~~
你可以修改復制到本地的代碼了( symfony-docs-chs 項目里都是 rst 格式的文檔)。當你覺得完成了一定的工作量,想做個階段性的提交:
向這個本地的代碼倉庫添加當前目錄的所有改動:
~~~
$ git add .
~~~
或者只是添加某個文件:
~~~
$ git add -p
~~~
我們可以輸入
~~~
$ git status
~~~
來看現在的狀態,如下圖是添加之前的:

Before add
下面是添加之后 的

After add
可以看到狀態的變化是從黃色到綠色,即 unstage 到 add。
在完成添加之后,我們就可以寫入相應的提交信息——如這次修改添加了什么內容 、這次修改修復了什么問題等等。在我們的工作流程里,我們使用 Jira 這樣的工具來管理我們的項目,也會在我們的 Commit Message 里寫上作者的名字,如下:
~~~
$ git commit -m "[GROWTH-001] Phodal: add first commit & example"
~~~
在這里的`GROWTH-001`就相當于是我們的任務號,Phodal 則對應于用戶名,后面的提交信息也會寫明這個任務是干嘛的。
由于有測試的存在,在完成提交之后,我們就需要運行相應的測試來保證我們沒有破壞原來的功能。因此,我們就可以PUSH我們的代碼到服務器端:
~~~
$ git push
~~~
這樣其他人就可以看到我們修改的代碼。
## Tasking
初到 ThoughtWorks 時,Pair 時候總會有人教我如何開始編碼,這應該也是一項基礎的能力。結合日常,重新演繹一下這個過程:
1. 有一個明確的實現目標。
2. 評估目標并將其拆解成任務(TODO)。
3. 規劃任務的步驟(TODO)
4. 學習相關技能
5. 執行 Task,遇到難題就跳到第二步。
### 如何 Tasking 一本書
以本文的寫作為例,細分上面的過程就是:
1. 我有了一個中心思想——在某種意義上來說就是標題。
2. 依據中心思想我將這篇文章分成了四小節。
3. 然后我開始寫四小節的內容。
4. 直到完成。
而如果將其劃分到一個編程任務,那么也是一樣的:
1. 我們想到做一個 xxx 的 idea。
2. 為了這個 idea 我們需要分成幾步,或者幾層設計。
3. 對于每一步,我們應該做點什么
4. 我們需要學習怎樣的技能
5. 集成每一步的代碼,就有了我們的系統。
現在讓我們以這本書的寫作過程為例,來看看這個過程是怎么發生的。
在計劃寫一本書的時候,我們有關于這本書主題的一些想法。正是一些想法慢慢地凝聚成一個穩定的想法,不過這不是我們所要討論的重點。
當我們已經有了一本書的相關話題的時候,我們會打算去怎么做?先來個頭腦風暴,在上面寫滿我們的一些想法,如這本書最開始劃分了這七步:
* 從零開始
* 編碼
* 上線
* 數據分析
* 持續交付
* 遺留系統
* 回顧與新架構
接著,依據我們的想法整理出幾個章節。如本書最初的時候只有七個章節,但是我們還需要第一個章節來指引新手,因此變成了八個章節。對應于每一個章節,我們都需要想好每一章里的內容。如在第一章里,又可以分成不同的幾部分。隨后,我們再對每一部分的內容進行任務劃分,那么我們就會得到一個又一個的小的章節。在每個小的章節里,我們都可以大概策劃一下我們要寫的內容。
然后我們就可以開始寫這樣的一本書——由一節節匯聚成一章,由一章一章匯聚成一本。
### Tasking 開發任務
現在,讓我們簡單地來 Tasking 如何開發一個博客。作為一個程序員,如果我們要去開始一個博客系統的話,那么我們會怎么做?
1. 先規劃一下我們所需要的功能——如后臺、評論、Social 等等,并且我們還應該設計我們博客的 Mockup。
2. 隨后我們就可以簡單地設計一下系統的架構,如傳統的前后端結合。
3. 我們就可以進行技術選型了——使用哪個后端框架、使用哪個前端框架。
4. 創建我們的 hello,world,然后開始進行一個功能的編碼工作。
5. 編碼時,我們就需要不斷地查看、添加測試等等。
6. 完成一個個功能的時候,我們就會得到一個子模塊。
7. 依據一個個子模塊,我們就可以得到我們的博客系統。
與我們日常開發一致的是:我們需要去劃分任務的優先級。換句話來說,我們需要先實現我們的核心功能。
對于我們的博客系統來說,最主要的功能就是發博客、展示博客。往簡單地說,一篇博客應該有這么基礎的四部分:
1. 標題
2. 內容
3. 作者
4. 時間
5. Slug
然后,我們就需要創建相應的 Model,根據這個 Model,我們就可以創建相應的控制器代碼。再配置下路由,添加下頁面。對于有些系統來說,我們就可以完成博客系統的展示了。
## 寫代碼只是在碼字
編程這件事情實際上一點兒也不難,當我們只是在使用一個工具創造一些東西的時候,比如我們拿著電烙鐵、芯片、電線等去焊一個電路板的時候,我們學的是如何運用這些工具。雖然最后我們的電路板可以實現相同的功能,但是我們可以一眼看到差距所在。
換個貼切一點的比喻,比如燒菜做飯,對于一個優秀的廚師和一個像我這樣的門外漢而言,就算給我們相同的食材、廚具,一段時間后也許一份是誘人的美食,一份只能喂豬了——即使我模仿著廚師的步驟一步步地來,也許看上去會差不多,但是一吃便吃出差距了。
我們還做不好飯,還焊不好電路,還寫不好代碼,很大程度上并不是因為我們比別人笨,而只是別人比我們做了更多。有時候一種機緣巧遇的學習或者 bug 的出現,對于不同的人的編程人生都會有不一樣的影響(ps:說的好像是蝴蝶效應)。我們只是在使用工具,使用的好與壞,在某種程序上決定了我們寫出來的質量。
寫字便是如此,給我們同樣的紙和筆(ps:減少無關因素),不同的人寫出來的字的差距很大,寫得好的相比于寫得不好的 ,只是因為練習得更多。而編程難道不也是如此么,最后寫代碼這件事就和寫字一樣簡單了。
剛開始寫字的時候,我們需要去了解一個字的筆劃順序、字體結構,而這些因素相當于語法及其結構。熟悉了之后,寫代碼也和寫字一樣是簡簡單單的事。
#### 學習編程只是在學造句
> ?多么無聊的一個標題
`計算機語言同人類語言一樣`,有時候我們也許會感慨一些計算機語言是多么地背離我們的世界,但是他們才是真正的計算機語言。
計算機語言是模仿人類的語言,從 if 到其他,而這些計算機語言又比人類語言簡單。故而一開始學習語言的時候我們只是在學習造句,用一句話來概括一句代碼的意思,或者可以稱之為函數、方法(method)。
于是我們開始組詞造句,以便最后能拼湊出一整篇文章。
#### 編程是在寫作
> ?編程是在寫作,這是一個怎樣的玩笑?這是在諷刺那些寫不好代碼,又寫不好文章的么
代碼如詩,又或者代碼如散文。總的來說,這是相對于英語而言,對于中文而言可不是如此。**如果用一種所謂的中文語言寫出來的代碼,不能像中文詩一樣,那么它就算不上是一種真正的中文語言。**
那些所謂的寫作邏輯對編程的影響
* 早期的代碼是以行數算的,文章是以字數算的
* 代碼是寫給人看的,文章也是寫給人看的
* 編程同寫作一樣,都由想法開始
* 代碼同文章一樣都可以堆砌出來(ps:如本文)
* 寫出好的文章不容易,需要反復琢磨,寫出好的代碼不也是如此么
* 構造一個類,好比是構造一個人物的性格特點,多一點不行,少一點又不全
* 代碼生成,和生成詩一樣,沒有情感,過于機械化
* 。。。
然而好的作家和一般的寫作者,區別總是很大,對同一個問題的思考程度也是不同的。從一個作者到一個作家的過程,是一個不斷寫作不斷積累的過程。而從一個普通的程序員到一個優秀的程序員也是如此,需要一個不斷編程的過程。
當我們開始真正去編程的時候,我們還會糾結于“**僧推月下門**”還是“**僧敲月下門**”的時候,當我們越來越熟練就容易決定究竟用哪一個。而這樣的“推敲”,無論在寫作中還是在編程中都是相似的過程。
> 寫作的過程真的就是一次探索之旅,而且它會貫穿人的一生。
因此:
> 編程只是在碼字,難道不是么?
真正的想法都在腦子里,而不在紙上,或者 IDE 里。
## 內置索引與外置引擎
### 門戶網站
讓我們先來看看門戶網站。
百科上說:
> 門戶網站(英語:Web portal,又稱入口網站,入門網站)指的是將不同來源的信息以一種整齊劃一的形式整理、儲存并呈現的網站
從某種意義上來說門戶網站更適合那些什么都不知道,從頭開始探索互聯網的人。換句話說,這類似于有點于類似我們在學第一門計算機語言——我們不需要去尋找什么,我們也不知道一些復雜的概念。
這時候我們只能隨便的看一本別人推薦的書籍,讀一讀別人寫的筆記,開始一點點構建我們的知識體系。
而在我們學習第二門計算機語言的時候,我們有了更多的訣竅——我們知道怎么去搜索。在我們的知識體系里,我們知道如何去搜索,這時我們就可以通過搜索引擎來學習。
百科上大致將搜索引擎分成了四部分:搜索器、索引器、檢索器、用戶接口。
1. 搜索器:其功能是在互聯網中漫游,發現和搜集信息。
2. 索引器:其功能是理解搜索器所搜索到的信息,從中抽取出索引項,用于表示文檔以及生成文檔庫的索引表。
3. 檢索器:其功能是根據用戶的查詢在索引庫中快速檢索文檔,進行相關度評價,對將要輸出的結果排序,并能按用戶的查詢需求合理反饋信息。
4. 用戶接口:其作用是接納用戶查詢、顯示查詢結果、提供個性化查詢項。
我想這部分大家都是有點印象的就不多介紹了(即:Ctrl + C, Ctrl + V)。
對于一個新手來說,使用搜索引擎的最大障礙就是——你知道問題,但是你不知道怎么搜索。這也是為什么,你會在那么多的博客、問答里,看到如何使用搜索引擎。
但是這并不能解決根本性問題——你需要知道你的問題是什么。順便,推薦一本書叫做《你的燈亮著嗎?》
### 內置索引與外置引擎
(ps: 為了和搜索引擎對應起來,這里就將內置門戶改成內置索引。)
所以,再仔細回到上面的問題里。要成為一名可以完成任務的程序員,你就需要不斷地構建你的門戶網站。我們要學習 Web 開發,我們就需要對整個知識體系有一個好的理解。不斷理解的過程中,我們就不斷也添加了新的文檔,構建新的索引。每遇到一個新的知識點,我們就開始重新生成新的索引。
然后又會引入一個問題:
> 人的大腦如同一間空空的閣樓,要有選擇地把一些家具裝進去。
我們需要不斷地整理一些新的技術,并且想方設法地忘記舊的知識。
有時,不得不說筆記和博客是這樣一個很好的載體。在未來的某一天,我們可以重新挖掘這些技術,識別技術的舊有缺陷,發展出新的技術——水能載舟,亦能覆舟。
## 如何編寫測試
寫測試相比于寫代碼來說算是一種簡單的事。多數時候,我們并不需要考慮復雜的邏輯。我們只需要按照我們的代碼邏輯,對代碼的行為進行覆蓋。
需要注意的是——在不同的團隊、工作流里,測試可能是會有不同的工作流程:
* 開發人員寫單元測試、集成測試等等
* 測試團隊通過界面來做黑盒測試
* 測試人員手動測試來測試功能
在允許的情況下,測試應該由開發人員來編寫,并且是由底層開始寫測試。為了更好地去測試代碼,我們需要了解測試金字塔。
### 測試金字塔
測試金字塔是由 Mike Cohn 提出的,主要觀點是:底層單元測試應多于依賴 GUI 的高層端到端測試。其結構圖如下所示:

測試金字塔
從結構上來說,上面的金字塔可以分成三部分:
1. 單元測試。
2. 服務測試
3. UI 測試
從圖中我們可以發現:單元測試應該要是最多的,也是最底層的。其次才是服務測試,最后才是 UI 測試。大量的單元測試可以保證我們的基礎函數是正常、正確工作的。而服務測試則是一門很有學問的測試,不僅僅只在測試我們自己提供的服務,也會測試我們依賴第三方提供的服務。在測試第三方提供的服務時,這就會變成一件有意思的事了。除此還有對功能和 UI 的測試,寫這些測試可以減輕測試人員的工作量——畢竟這些工作量轉向了開發人員來完成。
#### 單元測試
單元測試是針對程序模塊(軟件設計的最小單位)來進行正確性檢驗的測試工作。它是應用的最小可測試部件。舉個例子來說,下面是一個JavaScript 的函數,用于判斷一個變量是否是一個對象:
~~~
var isObject = function (obj) {
var type = typeof obj;
return type === 'function' || type === 'object' && !!obj;
};
~~~
這是一個很簡單的功能,對應的我們會有一個簡單的 Jasmine 測試來保證這個函數是正常工作的:
~~~
it("should be a object", function () {
expect(l.isObject([])).toEqual(true);
expect(l.isObject([{}])).toEqual(true);
});
~~~
雖然這個測試看上去很簡單,但是大量的基本的單元測試可以保證我們調用的函數都是可以正常工作的。這也相當于是我們在建設金字塔時用的石塊——如果我們的石塊都是經常測試的,那么我們就不怕金字塔因為石塊的損壞而坍塌。
當單元測試達到一定的覆蓋率,我們的代碼就會變得更健壯。因為我們都需要保證我們的代碼都是可測的,也意味著我們代碼間的耦合度會降低。我們需要去考慮代碼的長度,越長的代碼在測試的時間會變得越困難。這也就是為什么 TDD 會促使我們寫出短的代碼。如果我們的代碼都是有測試的,單元測試可以幫助我們在未來重構我們的代碼。
并且在很多沒有文檔或者文檔不完整的開源項目中,了解這個項目某個函數的用法就是查看他的測試用例。測試用例(Test Case)是為某個特殊目標而編制的一組測試輸入、執行條件以及預期結果,以便測試某個程序路徑或核實是否滿足某個特定需求。這些測試用例可以讓我們直觀地理解程序程序的 API。
#### 服務測試
服務測試顧名思義便是對服務進行測試,而服務可以是有不同的類型,不同層次的測試。如第三方的 API 服務、我們程序提供的服務,雖然他們他應該在這一個層級上進行測試,但是對他們的測試會稍有不同。
對于第三方的提供的 API 服務或者其他類似的服務,在這一個層級的測試,我們都不會真實地去測試他們能不能工作——這些依賴性的服務只會在功能測試上進行測試。在這里的測試,我們只會保證我們的功能代碼是可以正常工作的,所以我們會使用一些虛假的 API 測試數據來進行測試。這一類提供 API 的 Mock Server 可以模擬被測系統外部依賴模塊行為的通用服務。我們只要保證我們的功能代碼是正常工作的,那么依賴他的服務也會是正常工作的。

Mock Server
而對于我們提供的服務來說,這一類的服務不一定是 API 的服務,還有可能是多個函數組成的功能性服務。當我們在測試這些服務的時候,實際上是在測試這個函數結合在一起是不是正常的。
一個服務可能依賴于多個函數,因而我們會發現服務測試的數量是少于單元測試的。
#### UI 測試
在傳統的軟件開發中,UI 測試多數是由人手動來完成的。而在稍后的章節里,你將會看到這些工作是可以由機器自己來完成的——當然,前提是我們要編寫這些自動化測試的代碼。需要注意的是 UI 測試并不能完全替代手工的工作,一些測試還是應該由人來進行測試——如對 UI 的布局,在現階段機器還沒有審美意識呢。
自動化 UI 測試是一個緩慢的過程,在這個過程里我們需要做這么幾件事:
1. 運行起我們的網站——這可能需要幾分鐘。
2. 添加一些 Mock 的數據,以使網站看上去正常——這也需要幾分鐘到幾十分鐘的時間。
3. 開始運行測試——在一些依賴于網絡的測試中,運行完一個測試可能會需要幾分鐘。盡管可以并行運行測試,但是一個測試幾分鐘算到最后就會累積成很長的時間。
所以,你會發現這是一個很長的測試過程。盡可能地將這個層級的測試往下層級移,就會盡可能的節省時間。一個 UI 測試需要幾分鐘,但是一個單元測試可能不到1秒。這就意味著,這樣的測試下移可以節省上百個數量級的時間。
### 如何測試
現在問題來了,我們應該怎么去寫測試?換句話來說,我要測什么?這是一個很難的問題,這足夠可以以一本書的幅度來說明這個問題。這個問題也需要依賴于不同的實踐,不同的時候我們可能對問題的看法都有不同。
編寫測試的過程大致可以分成下面的幾個步驟:
1. 了解測試目的(Why)?即我們需要測什么,我們是為了什么而編寫的測試。
2. 我們要測哪些內容(What)?即測試點,我們即要從功能點上出發來尋找需要我們測試的點,在不同的條件下這個測試點是不一樣的。
3. 我們要如何進行測試(How)?我們要使用怎么樣的方法進行測試?
#### 測試目的
我們在上面提到過的測試金字塔,也表明了我們在每個層級要測試的目的是不一樣的。
在單元測試這一層級,因為我們所測試的是每一個函數,這些函數沒有辦法構成完成的功能。這時候我們就只是用于簡簡單單的測試函數本身的功能,沒有太多的業務需求。
而對于服務這一層級,我們所要測試的就是一個完整的功能。對于以 API 為主的項目來說,實際上就是在測返回結果是否是正確的。
最后 UI 這一層級,我們所需要測試的就是一個完整的功能。用戶操作的時候應該是怎樣的,那么我們就應該模仿用戶的行為來測試。這是一個完整的業務需求,也可以稱之為驗證測試。
#### 測試點
在了解完我們要測試的目的之后,我們要測試的點也變得很清晰。即在單元測試測試我們的函數的功能,在我們的服務測試我們的服務,在我們的 UI測試測試業務。
而這些都理想的情況,當系統由于業務的原因不得不耦合的時候。究竟是單元測試還是功能測試,這是一個特別值得思考的問題。如果一個功能即可以在單元測試里測,又可以在服務測試里測,那么我們要測試哪一個?或者說我們應該把兩個都測一遍?而如果是花費時間更長的 UI 測試呢?這樣做是不是會變得不劃算。
#### 如何寫測試代碼
先讓來們來簡單地看一下測試用例,然后再讓我們看看一般情況下我們是如何寫測試代碼的。下面的代碼是一個用Python寫的測試用例:
~~~
class HomepageTestCase(LiveServerTestCase):
def setUp(self):
self.selenium = webdriver.Firefox()
self.selenium.maximize_window()
super(HomepageTestCase, self).setUp()
def tearDown(self):
self.selenium.quit()
super(HomepageTestCase, self).tearDown()
def test_can_visit_homepage(self):
self.selenium.get(
'%s%s' % (self.live_server_url, "/")
)
self.assertIn("Welcome to my blog", self.selenium.title)
~~~
在上面的代碼里主要有三個方法,setUp()、tearDown()和 test_can_visit_homepage()。在這三個方法中起主要作用的是 test_can_visit_homepage()方法。而 setUp() 和 tearDown() 是特殊的方法,分別在測試方法開始之前運行和之后運行。同時,在這里我們也用這兩個方法來打開和關閉瀏覽器。
而在我們的測試方法 test_can_visit_homepage() 里,主要有兩個步驟:
1. 訪問首頁
2. 驗證首頁的標題是“Welcome to my blog”
大部分的測試代碼也是以如何的流程來運行著。有一點需要注意的是:一般來說函數名就表示了這個測試所要做測試的事情,如這里就是測試可以訪問首頁。
如上所示的測試過程稱為“四階段測試”,即這個過程分為如下的四個階段:
1. **Setup**。在這個階段主要是做一些準備工作,如數據準備和初始化等等,在上面的 setup 階段就是用 selenium 啟動了一個 Firefox 瀏覽器,然后把窗口最大化了。
2. **Execute**。在執行階段就是做好驗證結果前的工作,如我們在測試注冊的時候,那么這里就是填寫數據,并點擊提交按鈕。在上面的代碼里,我們只是打開了首頁。
3. **Verify**。在驗證階段,我們所要做的就是驗證返回的結果是否和我們預期的一致。在這里我們還是使用和單元測試一樣的 assert 來做斷言,通過判斷這個頁面的標題是“Welcome to my blog”,來說明我們現在就是在首頁里。
4. **Tear Down**。就是一些收尾工作啦 ,比如關閉瀏覽器、清除測試數據等等。
#### Tips
需要注意的幾點是:
1. 從運行測試速度上來看,三種測試的運行速度是呈倒金字塔結構。即,單元測試跑得最快,開發速度也越快。隨后是服務測試,最后是 UI 測試。
2. 即使現在的 UI 測試跑得非常快,但是隨著時間的推移,UI 測試會越來越多。這也意味著測試來跑得越來越久,那么人們就開始不想測試了。在我們之前的項目里,運行完所有的測試大概接近一個小時,我們開始在會議會爭論這些測試的必要性,也在想方設法減少這些測試。
3. 如果一個測試可以在最底層寫,那么就不要在他的上一層寫了,因為他的運行速度更快。
參考書籍:
* 《優質代碼——軟件測試的原則、實踐與模式》
* 《Python Web 開發: 測試驅動開發方法》
## 測試替身
測試替身(Test Double)是一個非常有意思的概念。
> 有時候對被測系統(SUT)進行測試是很困難的,因為它依賴于其他無法在測試環境中使用的組件。這有可能是因為這些組件不可用,它們不會返回測試所需要的結果,或者執行它們會有不良副作用。在其他情況下,我們的測試策略要求對被測系統的內部行為有更多控制或更多可見性。 如果在編寫測試時無法使用(或選擇不使用)實際的依賴組件(DOC),可以用測試替身來代替。測試替身不需要和真正的依賴組件有完全一樣的的行為方式;他只需要提供和真正的組件同樣的 API 即可,這樣被測系統就會以為它是真正的組件! ——Gerard Meszaros
當我們遇到一些難以測試的方法、行為的時候,我們就一些特別的方式來幫助我們測試。Mock 和 Stub 就是常見的兩種方式:
1. Stub 是一種狀態確認,它用簡單的行為來替換復雜的行為
2. Mock 是一種行為確認,它用于模擬其行為
通俗地來說:Stub 從某種程度上來說,會返回我們一個特定的結果,用代碼替換來方法;而 Mock 只是確保這個方法被調用。
### Stub
Stub 從字面意義上來說是存根,存根可以理解為我們保留了一些預留的結果。這個時候我們相當于構建了這樣一個特殊的測試場景,用于替換諸如網絡或者 IO 調度等高度不可預期的測試。如當我們需要去驗證某個 API 被調用并返回了一個結果,舉例在最小物聯網系統設計中返回的 json,我們可以在本地構建一個
~~~
[{"id":1,"temperature":14,"sensors1":15,"sensors2":12,"led1":1}]
~~~
的結果來當我們預期的數據,也就是所謂的存根。那么我們所要做的也就是解析 json,并返回預期的結果。當我們依賴于網絡時,此時測試容易出現問題。
### Mock
Mock 從字面意義上來說是模仿,也就是說我們要在本地構造一個模仿的環境,而我們只需要驗證我們的方法被調用了。
~~~
var Foo = function(){};
Foo.prototype.callMe = function() {};
var foo = mock( Foo );
foo.callMe();
expect( foo.callMe ).toHaveBeenCalled();
~~~
## 測試驅動開發
測試驅動開發是一個很“古老”的程序開發方法,然而由于國內的開發流程的問題——即開發人員負責功能的測試,導致這么好的一項技術沒有在國內推廣。
### 紅-綠-重構
測試驅動開發的主要過程是: 紅 —> 綠 -> 重構

TDD
1. 先寫一個失敗的單元測試。即我們并沒有實現這個方法,但是已經有了這個方法的測試。
2. 讓測試通過。實現簡單的代碼來保證測試通過,就算我們用一些作弊的方法也是可以的。我們寫的是功能代碼,那么我們應該提交代碼,因為我們已經實現了這個功能。
3. 重構,并改進功能代碼,讓它變得更加合理。
TDD 有助于我們將問題分解成更小的部分,再一點點的添加我們所需要的業務代碼。隨著這個過程的不斷進行,我們會發現我們已經接近完成我們的功能代碼了。并且到了最后,我們會發現我們的代碼都會被測試到。
雖然說起來看上去很簡單,但是真正實現起來并不是那么容易。于我而言我只會在我自己造的一些輪子中使用 TDD。因為這個花費大量的時間,通常來說測試代碼和功能代碼的比例可能是1:1,或者是2:1等等。在自己創建的一些個人應用,如博客中,我不需要與其他人 Share 我的 Content。由于我使用的是第三方框架,框架本身的測試已經足夠多,并且沒有復雜的邏輯,我就沒有對我的博客寫測試。而在我寫的一些框架里,我就會盡量保證足夠高的測試覆蓋率,并且在適當的時候會去 TDD。
通常來說對于單元測試我會采用 TDD 的方式來進行,但是功能測試仍會選擇在最后添加進去。主要的緣由是:在寫 UI 的過程中,元素會發生變化。這一點和我們在寫 Unit 的時候,有很大的區別。div + class 會使得我們思考問題的方式發生變化,我們需要去點擊某個元素,并觀察某個元素發生的變化。而多數時候,我們很難把握好一個頁面最好的樣子。
不得不說明的一點是,TDD 需要你對測試比較了解后,你才容易使用它。從個人的感受來說,TDD 是在一開始是一件很難的事。
### 測試先行
對于寫測試的人來說,測試先行有點難以理解,而對于不寫測試的人來說,就更難以理解。這里假定你已經開始寫測試了,因為對于不寫測試的人來說,寫測試就是一件難以理解的事。既然我們都要寫測試,那么為什么我們就不能先寫測試呢?或者說為什么后寫測試存在一些問題?
依據 J.Timothy King 所總結的《測試先行的12個好處》:
1. 測試可證明你的代碼是可以解決問題的
2. 一面寫單元測試,一面寫實現代碼,這樣感覺更有興趣
3. 單元測試也可以用于演示代碼
4. 會讓你在寫代碼之前做好計劃
5. 它降低了 Bug 修復的成本
6. 可以得到一個底層模塊的回歸測試工具
7. 可以在不改變現有功能的基礎上繼續改進你的設計
8. 可以用于展示開發的進度
9. 它真實的為程序員消除了工作上的很多障礙
10. 單元測試也可以讓你更好的設計
11. 單元測試比代碼審查的效果還要好
12. 它比直接寫代碼的效率更高
但是在我個人的感覺里,多比較喜歡的是:?**寫出可以測試的函數**。這是一個一直困擾著我的難題,特別是當我的代碼里存在很多條件的時候,在后期我編寫的時候,難度越來越大。當我只有一個簡單的 IF-ELSE 的時候,我的代碼測試起來也很簡單:
~~~
if (hour < 18) {
greeting = "Good day";
} else {
greeting = "Good evening";
}
~~~
而當我有復雜的業務邏輯時,后寫測試就會變成一場惡夢:
~~~
if (EchoesWorks.isObject(words)) {
var nextTime = that.parser.parseTime(that.data.times)[currentSlide + 1];
if (that.time < nextTime && words.length > 1) {
var length = words.length;
var currentTime = that.parser.parseTime(that.data.times)[currentSlide];
var time = nextTime - currentTime;
var average = time / length * 1000;
var i = 0;
document.querySelector('words').innerHTML = words[i].word;
timerWord = setInterval(function () {
i++;
if (i - 1 === length) {
clearInterval(timerWord);
} else {
document.querySelector('words').innerHTML = words[i].word;
}
}, average);
}
return timerWord;
} else {
document.querySelector('words').innerHTML = words;
}
~~~
我們需要重新理清業務的邏輯,再依據這些邏輯來編寫測試代碼。而當我們已經忘記具體的業務邏輯時,我們已然無法寫出測試。
**思考**
通常在我的理解下,TDD 是可有可無的。既然我知道了我要實現的大部分功能,而且我也知道如何實現。與此同時,對 Code Smell 也保持著警惕、要保證功能被測試覆蓋。那么,總的來說 TDD 帶來的價值并不大。
然而,在當前這種情況下,我知道我想要的功能,但是我并不理解其深層次的功能。我需要花費大量的時候來理解,它為什么是這樣的,需要先有一些腳本來知道它是怎么工作的。TDD 變顯得很有價值,換句話來說,在現有的情況下,TDD 對于我們不了解的一些事情,可以驅動出更多的開發。畢竟在我們完成測試腳本之后,我們也會發現這些測試腳本成為了代碼的一部分。
在這種理想的情況下,我們為什么不 TDD 呢?
參考資料
J.Timothy King 《Twelve Benefits of Writing Unit Tests First》
## 可讀的代碼
過去,我有過在不同的場合吐槽別人的代碼寫得爛。而我寫的僅僅是比別人好一點而已——而不是好很多。
然而這是一件很難的事,人們對于同一件事物未來的考慮都是不一樣的。同樣的代碼在相同的情景下,不同的人會有不同的設計模式。同樣的代碼在不同的情景下,同樣的人會有不同的設計模式。在這里,我們沒有辦法討論設計模式,也不需要討論。
我們所需要做的是,確保我們的代碼易讀、易測試,看上去這樣就夠了,然而這也是挺復雜的一件事:
* 確保我們的變量名、函數名是易讀的
* 沒有復雜的邏輯判斷
* 沒有多層嵌套 (事不過三)
* 減少復雜函數的出現(如,不超過三十行)
* 然后,你要去測試它。這樣你就知道需要什么,實際上要做到這些也不是一些難事。
只是首先,我們要知道我們要自己需要這些。對于沒有太多編程經驗的人,建議先從兩個基本點做起:
* 命名
* 函數長度
首先要說的就是程序員認為最難的一個話題了——命名。
### 命名
命名是一個特別長的,也是特別憂傷的故事。我想作為一個程序員的你,也相當恐懼這件事。一個好的函數名、變量名應該包含著這個函數的信息,如這個函數是干什么的,或者這個函數是怎么來的,這個變量名存儲的是什么。
正因為取名字是一件很重要的事,所以它也是一件很難的事。一個好的函數名、變量名應該能正確地表達出它的涵義。如你可以猜到下面的代碼中的i是什么意思嗎?
~~~
fruits = ['banana', 'apple', 'mango']
for i in fruits: # Second Example
print 'Current fruit :', i
~~~
那如果換成下面的代碼會不會更容易閱讀呢?
~~~
fruits = ['banana', 'apple', 'mango']
for fruit in fruits: # Second Example
print 'Current fruit :', fruit
~~~
而命令還存在于對函數的命名上,如我們可能會用 getNumber 來表示去獲取一個數值,但是要知道這樣的命名并不是在所有的語言中都可以這樣用。如在 Java 中存在 getter 和 setter 這種模式,如下的代碼所示:
~~~
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
~~~
如果我們是去取某個東西的數值,那么我們應該使用 retrieveNumber 這樣的更具代表性的名字。
在《編寫可讀代碼的藝術》也提到了這幾點:
1. 選擇專業的詞。最好是可以和業務相關的,它應該極具表現力。
2. 避免像 tmp 和 retval 這樣泛泛的名字。不得不提到的一點是,tmp 實在是一個有夠爛的名字,將其變為 timeTemp 或者類似的會更直觀。它只應該是名字中的一部分。
3. 用具體的名字代替抽象的名字。
4. 為名字賦予更多的信息。
5. 名字應該有多長。
6. 利用名字的格式來傳遞含義。
### 函數長度
> 函數是指一段在一起的、可以做某一件事兒的程序。
這就意味著從定義上來說,這段函數應該只做一件事——但是什么才是真正的一件事呢?實際上還是 TASKING,將一個復雜的過程一步步地分解成一個個的函數,每個函數只做他的名稱對應的事。對于一個任務來說,他有一個穩定的過程,在這個過程中的每一步都可以變成一個函數。
因此,長的代碼意味著一件事——這個函數可能違反了單一職責原則,即這個類做了太多的事。通常來說,一個類,只有一個引起它變化的原因。當一個類有多個職責的時候,這些代碼就容易耦合到一起了。
對于函數長度的控制是為了有效控制分支深度。如果我們用一個函數來實現一個復雜的功能,那么不僅僅在我們下次閱讀的時間會花費大量的時間。而且如果我們的代碼沒有測試話,那么這些代碼就會變得越來越難以理解。而在我們寫這些函數的時候就沒有測試,那么這個函數就會變得越來越難以測試,它們就會變成遺留代碼。
### 其他
雖然只想介紹上面的簡單的兩點,但是順便在這里也提一下重復代碼~~。
#### 重復代碼
在《重構》一書中首先提到的 Code Smell 就是重復代碼(Duplicate Code)。重復代碼看上去并不會影響我們的閱讀體驗,但是實際上會發生這樣的事——重復的代碼閱讀體驗越不好。
DRY(Don’t Repeat Yourself)原則是特別值得玩味的。當我們不斷地偏執的去減少重復代碼的時候,會導致復雜度越來越高。在適當的時候,由于業務發生變更,我們還需要去拆解這些不重復的代碼。
## 代碼重構
> 重構,一言以蔽之,就是在不改變外部行為的前提下,有條不紊地改善代碼。
代碼重構(英語:Code refactoring)指對軟件代碼做任何更動以增加可讀性或者簡化結構而不影響輸出結果。在經歷了一年多的工作之后,我平時的主要工作就是修 Bug。剛開始的時候覺得無聊,后來才發現修 Bug 需要更好的技術。有時候你可能要面對著一坨一坨的代碼,有時候你可能要花幾天的時間去閱讀代碼。而,你重寫那幾十代碼可能只會花上你不到一天的時間。但是如果你沒辦法理解當時為什么這么做,你的修改只會帶來更多的 Bug。修 Bug,更多的是維護代碼。還是前人總結的那句話對:
> 寫代碼容易,讀代碼難。
假設我們寫這些代碼只要半天,而別人讀起來要一天。為什么不試著用一天的時候去寫這些代碼,讓別人花半天或者更少的時間來理解。
### 重命名
在上一節中,我們提到了命名的重要性,這里首先要說到的也就是重命名~。讓再看看《編寫可讀代碼的藝術》也提到了這幾點:
1. 選擇專業的詞。最好是可以和業務相關的,它應該極具表現力。
2. 避免像 tmp 和 retval 這樣泛泛的名字。不得不提到的一點是,tmp 實在是一個有夠爛的名字,將其變為 timeTemp 或者類似的會更直觀。它只應該是名字中的一部分。
3. 用具體的名字代替抽象的名字。
4. 為名字賦予更多的信息。
5. 名字應該有多長。
6. 利用名字的格式來傳遞含義。
### 提取變量
先讓我們來看看一個簡單的情況:
~~~
if ($scope.goodSkills.indexOf('analytics') !== -1) {
skills.analytics = 5;
}
~~~
在上面的代碼里比較難以看懂的就是數字5,這時候你會怎么做?寫一行注釋?這里的5就是一個 Magic Number。
而實際上,最簡單有效的辦法就是把5提取成一個變量:
~~~
var LEVEL_FIVE = 5;
if ($scope.goodSkills.indexOf('analytics') !== -1) {
skills.analytics = LEVEL_FIVE;
}
~~~
### 提煉函數
這個簡單有效的方法就是為了對付之前太長的函數,抽取提煉函數出應該抽取出來的部分成為一個新的函數。引自《重構》一書的說法,短的精巧的函數有以下的特點:
1. 如果每個函數的粒度都很小,那么函數被復用的機會就更大;
2. 是這會讓高層函數讀起來就像一系列注釋一樣,容易理解;
3. 是如果函數都是細粒度,那么函數的復寫也會更加容易。
在提煉函數中我們所要做的就是——判斷出原有的函數的意圖,再依據我們的新意圖來命名新的函數。然后判斷依賴——變量值,處理這些變量。提取出函數,最近對其測試。
這里只簡單地對重構進行一些介紹,更多詳細信息請參閱《重構:改善既有代碼的設計》。
## Intellij Idea 重構
下面簡單地介紹一下,一些可以直接使用 IDE 就能完成的重構。這種重構可以用在日常的工作中,只需要使用 IDE 上的快捷鍵就可以完成了。
### 提煉函數
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`+`N`
Windows/Linux:?`Ctrl`+`Alt`+`N`
鼠標: Refactor | Inline
**重構之前**
~~~
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
~~~
### 查詢取代臨時變量
快捷鍵
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
~~~
2. 對其進行`Extrace Method`
3. 選擇`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);
}
}
~~~
## 重構到設計模式
> 模式和重構之間存在著天然聯系,模式是你想到達的目的地,而重構則是從其他地方到達這個目的地的條條道路——Martin Fowler《重構》
### 過度設計與設計模式
過度設計和設計模式是兩個很有意思的詞語,這取決于我們是不是預先式設計。通過以往的經驗我們很容易看到一個環境來識別一個模式。遺憾的是使用設計模式來依賴于我們整個團隊的水平。對于了解設計模式的人來說,設計模式就是一種溝通語言。而對于了解一些設計模式的人來說,設計模式就是復雜的代碼。
并且在軟件迭代的過程中需求總是不斷變化的,這就意味著如果我們對我們的代碼設計越早,那么在后期失敗的概率也就越大。設計會伴隨著需求而發生變化,在當時看起來合理的設計,在后期就會因此而花費過多的代價。
而如果我們不進行一些設計,就有可能出現設計不足。這種情況可能出現于沒有時間寫出更好的代碼的項目,在這些項目里由于一些原因出現加班等等的原因,使得我們沒有辦法寫出更好的代碼。同時,也有可能是因為參考項目的程序員的設計方面出現不足。
我們沒有對設計模式介紹的一個原因是——它需要有大量的編程經驗,才可以讓我們實現:重構到設計模式。