#開發流程
一個完整的開發流程應該有這四步:分析->設計->編碼->測試。很多開發團隊往往只有編碼這邊,弱化了其他步驟,他們拿到需求就開始寫代碼, 寫著寫著發現有問題,要么是遇到一個難點解決不了,要么是發現要返回修改以前寫過的代碼, 要么是發現有大量的重復代碼,又不知道怎么封裝,只能將錯就錯。做好了分析和設計編碼時就不會有這么多問題, 做好了測試產品bug就少,產品質量才高。 下面我分別詳細講解一下這四步。
## 分析
分析的時候,我們要分析需求和難點。
分析需求的方法是做需求陳述處理,前面我提到過, 要區分做什么和怎么做,把這兩部分獨立出來,做什么是固定不變的, 而怎么做可能會經常變。我們再熟悉一下舉的那個例子:我們要做一個成員列表(如圖1-44),產品經理告訴我們要按姓名拼音排序。

圖1-44 成員列表的例子
我們有時候不能直接聽產品經理的,如果真寫死成按姓名拼音排序就沒有可擴展性了,比如某一天產品經理又告訴你需要把VIP會員提前,那么你只能再去修改排序的程序。這個需求始終不變的是排序,按姓名拼音只是排序的一種方法,我們在設計數據庫時應該把排序字段設置為數字而不是拼音,再寫一個拼音轉換為數字的算法即可,這樣在后面排序規則變化,比如VIP會員要提前,只是修改對應用戶數據庫的排序字段數值即可,不用大改程序。
我們可以用xmind做需求分析, 先把看見和聽見的所有需求一條一條的列出來。拿到產品原型的時候,一點一點看,把看見的每個地方都先列出來 。 然后在每條需求進行分析, 看看能否區分做什么和怎么做,如果能區分,在做這條需求后面建兩個分支,把分析結果寫上去,如圖1-45。

圖1-45 用xmind做分析
需求過了一遍后,我們需要思考程序哪兒可能有難點,再把難點列到xmind中,比如程序列表這個功能,難點可能有:1,如何把中文姓名轉換為拼音。2,如果把拼音轉換為數字。
然后再想這兩個難點的解決方案, 在網上搜索發現有中文轉換拼音的開源庫(overtrue/laravel-pinyin),我們直接用這個庫就行,把解決方案寫在xmind中對應難點的后面。另外,我們還應該做一個簡單的demo,測試一些是否真的能解決。
程序員的二元性思維比較多,往往認為不是“是”就是“非”, 一個難點不能完美的解決感覺就不能解決,我們要有優雅降級的思維。假設,我們要做一個特殊的緩存的功能,緩存到服務端是最完美的,但有難度,不好實現。這時候想想能不能優雅降級,不能緩存到服務端能否緩存到客戶端, 緩存到客戶端方案不完美,比如用戶可能會手動清空緩存,但是有緩存總比沒有好,那么我們能不能做呢?
我們再分析剛才第二個難點, 拼音轉換為數字的算法, 我們可能定義a轉換為1, b轉換為2,c為3。 但有一個問題,我們對人名的第一個字排序了,要不要對第二個字排序。 比如“張三”和“張飛”兩個人名, 第一張的拼音首字母都是Z ,我們還需要對“三”和“飛”轉換為拼音排序不? 要做完美的解決方案的話是要對第二個字進行排序的, 那么怎么排序? 假設我們在這里卡住了,暫時想不到好的解決方案,很多人就覺得這個功能做不了,不能做,我們能不能優雅降級一下,先只對第一個字最排序,以后想到其他解決方案再完善這一塊呢?
## 設計
設計這一步,要體現出程序怎么寫,我們要設計出數據庫的表結構、API接口以及前端頁面等。設計這步也可以在xmind中完成(如圖1-46)。

圖1-46 用xmind做設計
設計這部分一定要體現出程序怎么開發,不能還是列舉需求,要說明要建立什么程序文件、要建立什么類,以及類有哪些方法, 方法的具體處理過程,在xmind中經常會用這些短句: “當XXX條件時做XXX”,“調用XXX方法”。
xmind在列這些“處理過程”時,就能發現很多重復的邏輯,我們應該把這些重復的邏輯獨立成模塊(封裝為類或函數)。這些要封裝的模塊我們在列xmind的時候就都想好了,而不是邊寫代碼邊想,這樣會不斷的推翻以前的代碼再重寫,更加浪費時間。
做設計這步時,在設計類的時候注意前面說過的“正交設計、類要有專職,善于用委托”。類方法的處理過程要詳細到在實際寫代碼的時候照著xmind實現代碼即可,那個時候不用想程序邏輯了。
對于新手來說,寫代碼之前把所有細節都想到是有難度的,他們要邊寫邊想,寫到具體的地方才知道有什么細節。但這樣的習慣很不好,寫著寫著代碼邏輯就會變亂。一定要慢慢養成寫之前想清楚的習慣。從現在開始我們養成寫代碼之前列xmind做分析和設計的習慣, 寫完代碼后再對比之前列的xmind 看看哪些是之前沒有想到的,要慢慢積累經驗,時間長了分析和設計能做得越來越好。
我們在xmind中設計出有哪些接口和模塊后,還利于團隊的溝通和工作的分工。
xmind列好后開發人員要集體開會,所有人要理解某個模塊怎么寫程序, 并做好分工,評估每個模塊的開發時間, 因為xmind列的比較細,某個模塊的時間往往能以分鐘或小時來估計,如有些模塊還必須以天為單位估時間的話,那證明這個模塊還能細化。
我們分工好后,可以在trello看板上做時間排期, 看板的用法后面會詳細講解。
## 編碼
在編碼階段按照之前設計好的xmind編碼, 編程時還要防衛式的編程,這樣以后系統出問題我們能及時發現和修復。 下面詳細介紹一下防衛式編程。
有時候我們對自己寫的代碼很自信,認為“這絕不會發生...”,比如我們讀取某條數據,認為這條數據肯定不會為空,然后沒有加任何判斷代碼。 如果某種之前沒有考慮到的情況下,這條數據為空了,你的程序就有bug了。這時候你再一點一點慢慢去找問題,可能要花幾個小時才能找到原因,而如果之前做好了防衛式編程,數據為空時有報警,就能讓你馬上解決問題。 防衛式編程就是在你認為不可能發生的地方加上判斷代碼,即使情況發生了我們也知道。比如
```
//讀取一條用戶數據
$user=M('User')->find($id);
//如果你認為這條用戶數據肯定不為空,那么做個判斷
if(empty($user)){
//如果為空了,發個系統報警,讓開發人員知道。
warn('讀取用戶信息為空,用戶uid:'.$id);
}
```
做好防衛式編程后,用戶再向我們反饋bug,我們的第一反應是去看報警,一看報警就知道了,原來在某個情況下,讀取用戶信息可能為空。
上面示例代碼中`warn`函數為自己自定義的一個報警函數,我們可以把報警信息發到手機短信或郵箱。
我們在“解決問題的方法”這一節講到過有時候在找程序bug的時候,可能找的報錯信息不是真正bug的原因,之所以出現這種情況也是沒有做好防衛式編程,一個變量在賦值時就有問題了,那時沒有做判斷,在使用時程序才報錯,問題原因不是變量使用時的問題,而是賦值時的問題。這好比飛機遭到恐怖襲擊爆炸了,不一定是飛機的問題,可能是安檢的問題。
另外對于程序可能出現的性能問題也要做好判斷。
在產品初期用戶量不大的時候,我們追加的是簡單快速實現功能,然后上線收集用戶反饋,再完善調整產品。如果一開始就程序設計得很好, 考慮高并發情況, 很可能上線產品沒有高并發情況,甚至有因為用戶反饋不好功能要做調整,導致之前寫的代碼作廢。 所以一般產品初期時以簡單快速實現功能為主, 產品后期才考慮性能問題。 但我們要對簡單快速實現的代碼加一個判斷,讓我們知道什么時候應該重構代碼了。 舉一個例子,比如我們給系統用戶發郵件,剛開始用戶不多, 我們可以foreach循環來發郵件。 當用戶變多了,我們一定要知道這個地方的代碼需要重構了,這時候需要把foreach循環的代碼改為用隊列發郵件了。
```
//讀取所有用戶的郵箱
$users=M('user')->field('email')->select();
//判斷用戶比較多的時候報警通知開發人員
if(count($users)>5000){
warn('用戶數已經大于5000,需要重構發郵件的代碼了');
}
//foreach循環發郵件
foreach($users as $user){
send_mail($user['email'],'郵件標題','郵件內容');
}
```
## 測試
測試的目的是為了減少程序的bug,自己寫的程序自己一定要測試好了,確定沒有問題再交付給其他同事。 有的人工作經驗越久就越來越自信,認為自己寫的代碼一定沒有問題,慢慢養成了寫代碼不測試的習慣。這樣會影響團隊之間協作,在團隊其他成員那里印象也不好。
想想我們如果是這樣的一個工作方式: 后端告訴前端接口寫好了,前端十分驚訝覺得后端的工作效率好高呀, 自己要加快速度了,前端的程序還沒有寫到要調接口的地方。后端此時無所事事悠閑的聽著音樂。前端終于程序寫到調用接口的地方了,結果一調用接口發現接口還報程序語法錯誤,前端十分生氣但是還是壓住火告訴后端接口有問題。 后端回答“哦,是嗎?我看看” ,后端去查找程序問題時,前端真的沒事干了,只能處于等待狀態。 過一會兒后端告訴前端 “程序好了,可以了” ,前端再調用接口沒有語法錯誤了,但發現邏輯走不通, 前端終于怒了,大聲對后端說 “你寫程序能不能自己測試一下” 。 后端理直氣壯的回答“你反正都要調接口,不就相當于給我做測試了?”
這樣的工作方式效率是極其低的,后端是把本屬于自己的測試工作想讓前端測,后端在修改接口的時候前端只能等待,浪費前端的時間,前端也會很煩這樣的后端,不愿意和他合作。很多時候程序員和產品經理的合作也是這樣,程序員告訴產品經理程序寫好了,而產品經理測試有很多問題,程序員和產品經理的矛盾就此產生了。甚至如果沒有人給程序員把關,直接把產品呈現給了用戶, 用戶操作一兩步發現有問題就直接人走,他才沒有這么好心幫你測試,給你反饋問題。
做好測試,減少程序bug有下面四種方式:
* 1,人工測試
人工測試是最簡單的方法,自己寫的程序一定要自己測試,而且要早測試,程序寫多了再測試,遇到bug找程序問題可能不太好找。
人工測試至少要保證自己寫的程序沒有語法錯誤和邏輯性錯誤。
如果交付的程序都有語法錯誤,那肯定是沒有做測試的。
根據產品需求還要驗證邏輯是不是對的,有時候一個邏輯涉及到好幾處功能,這幾處都要連起來測試一下。
* 2,單元測試
我們在做人工測試的時候經常需要準備一些測試數據。比如:提交表單時填寫的數據、請求接口時的請求參數。下次再要測試這個功能又要重新填寫這些數據。“自動化”是程序員的生產力。人工測試也是能用測試自動化的, 我們可以寫“測試程序”來測試程序的功能,這樣那些每次都要填寫的數據,在測試程序里面只寫一次可以重復利用。下次要測試同一個功能,只要運行測試程序即可。很多人認為寫自動化測試程序很浪費時間,其實我們每次人工測試填寫測試數據同樣浪費時間,何不一勞永逸呢?
自動化程序測試又分好幾類, 有單元測試,集成測試,黑盒測試,端對端測試等等。
單元測試是人們提得最多的。各個編程語言都有單元測試框架, 如PHP有phpunit 、Java有junit 、 iOS有XCTest,而且iOS在創建項目時就有默認的單元測試代碼,足見蘋果對單元測試的重視。 大家要掌握一種你用的程序語言的單元測試框架。
單元是指一個不可在分的模塊,比如一個函數,一個類。某個功能可以由很多單元組成,它要調用多個函數或類。 而單元測試是要求我們從小單元開始測試,寫程序去測試函數或類,判斷函數或類的返回值是否正確。
寫好單元測試能強制讓我們把程序架構設計好,高耦合的程序是無法寫單元測試的。我們必須做到程序的低耦合,一個函數不會和很多函數有關系,才好做單元測試。
寫好單元測試也能幫助以后重構和修改代碼。我們往往修改一次代碼可能會影響其他多處代碼,容易產生bug,靠人工測試很容易漏測, 而如果有單元測試,我們只要跑一下單元測試程序就知道自己修改的代碼有沒有問題。
人們提倡TDD開發模式很多年了, TDD(Test Driven Development)測試驅動開發,也就是說開發代碼之前先寫測試代碼。但這是要正真做到TDD開發還是有難度,我們往往為了趕項目進度,程序員不愿意寫單元測試。能堅持寫單元測試的團隊不多, 在國內一個開發團隊能寫單元測試那么他們的開發能力一定是國內領先的。
* 3,做好報警
我們單元測試很難做到把所有地方都測試到,也就是單元測試的覆蓋率很難到達100%。 人工測試也不敢保證把所有地方都測試到了。我們可以做好系統報警,這樣即使用戶觸發了我沒有測試到的bug,我們也能收到系統報警,然后及時修復bug。
當用戶觸發到程序bug的時候,程序往往會有報錯,我們應該把程序報錯做成系統報警然后通知開發人員。
程序報錯一般分為兩種,一種是FatalError終止性報錯。一種是warning報錯。
FatalError終止性報錯是當程序出現嚴重錯誤(如語法錯誤)導致程序無法繼續運行,必須終止程序
warnning報錯 是程序認為出現了小錯誤了,但程序還能繼續運行,不道德但沒有違法,不會被抓起來。 舉兩個PHP的例子:用`file_get_contents` 讀取文件,如果文件不存在會報warning錯誤,但不終止程序,會把文件內容當作為空字符串處理,繼續執行下面的程序; 另一個例子:在使用不存在的數組下標時,也會報warning錯誤。 有的人認為warning報錯不重要,反正程序能正常運行,甚至把warning報錯屏蔽掉。但warnning報錯往往能反映程序有bug,比如使用了一個不存在的數組下標,可能是因為粗心把下標的單詞寫錯了,但如果屏蔽了warning報錯,這個粗心導致的bug很難被發現。
觸發程序報錯的有可能不是開發人員,而是用戶,用戶看不懂程序報錯,需要把報錯做成報警通知給開發者。 我以PHP為例,PHP可以用set_error_handler和register_shutdown_function來接管報錯,如:
```
<?php
set_error_handler('error_handler');
register_shutdown_function('fatalError');
error_handler($errno, $errstr, $errfile, $errline){
switch($errno){
case E_WARNING: $severity = 'E_WARNING'; break;
case E_NOTICE: $severity = 'E_NOTICE'; break;
case E_USER_ERROR: $severity = 'E_USER_ERROR'; break;
case E_USER_WARNING: $severity = 'E_USER_WARNING'; break;
case E_USER_NOTICE: $severity = 'E_USER_NOTICE'; break;
case E_STRICT: $severity = 'E_STRICT'; break;
case E_RECOVERABLE_ERROR: $severity = 'E_RECOVERABLE_ERROR'; break;
case E_DEPRECATED: $severity = 'E_DEPRECATED'; break;
case E_USER_DEPRECATED: $severity = 'E_USER_DEPRECATED'; break;
case E_ERROR: $severity = 'E_ERR'; break;
case E_PARSE: $severity = 'E_PARSE'; break;
case E_CORE_ERROR: $severity = 'E_CORE_ERROR'; break;
case E_COMPILE_ERROR: $severity = 'E_COMPILE_ERROR'; break;
case E_USER_ERROR: $severity = 'E_USER_ERROR'; break;
default: $severity= 'E_UNKNOWN_ERROR_'.$errno; break;
}
$msg="{$severity}: {$errstr} in {$errfile} on line {$errline}";
warn($msg);//調用報警函數
}
function fatalError(){
if ($e = error_get_last())
{
error_handler($e['type'],$e['message'],$e['file'],$e['line']);
}
}
//報警函數
function warn($msg){
//獲得調用棧
ob_start();
debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
$trace = ob_get_contents();
ob_end_clean();
//格式化調用棧
$trace = preg_replace ('/^#0\s+' . __FUNCTION__ . "[^\n]*\n/", '', $trace, 1);
$trace = preg_replace ('/^#(\d+)/me', '\'<br />#\' . ($1 - 1)', $trace);
$msg.=$trace;//報警信息加上調用棧
send_mail('upfy@qq.com','系統報警',$msg);
}
```
上面代碼set_error_handler函數告訴了程序如果有報錯執行error_handler函數 , error_handler有四個參數, $errno 為錯誤碼, $errstr 為錯誤信息, $errfile 為錯誤程序文件地址, $errline 錯誤所在文件的行數 。 $errno為整數可讀性不好,所以代碼中用switch判斷整數的值設置一個可讀性較好的字符串。 另外有debug_print_backtrace 獲得了調用棧, 報警信息中有調用棧的話方便我們分析程序的問題。
因為set_error_handler函數不能接管終止性的錯誤信息,所以程序有用register_shutdown_function 指定程序終止時要執行的函數fatalError。 fatalError中用error_get_last獲得錯誤信息并再調用error_handler函數。
上面的示例代碼只是簡單的舉例,正式的報警系統的代碼可能考慮的問題還有更多,簡單說一下下面兩個問題。
* 1,如何防止重復的報警信息
我們不能讓報警系統重復的報警信息一直發給開發者,比如有一個程序bug,但現在同時5千人在訪問這個程序,不能報警5千次吧。 我們可以把發過的報警信息緩存一段時間,發報警時查詢一下緩存里面有沒有一樣的信息,相同信息就不要再發了,緩存可以用memcache等模塊做, 緩存時間可以設置為5分鐘。這樣重復報警信息5分鐘只會發一次。
* 2,如何跟蹤觸發報警的用戶
很多用戶遇到產品有bug就直接走人,他才不會好心給你反饋問題。我們如果還想知道報警是哪個用戶觸發的,可以在報警代碼出讀取一個用戶信息一并寫到報警信息中,比如可以讀一下這個用戶的用戶名,手機號等有用信息。我們修復bug后還能聯系之前觸發了bug的用戶讓他回來繼續使用產品。
現在除了郵件、短信等來接收報警信息以外, 還有更好的工具可以用slack , slack既有PC客戶端也有手機客戶端,這樣發送的報警在電腦上能收到、在手機上也能收到。往slack上發送報警信息只要調用slack的接口即可,使用slack訪問官網:http://slack.com 。但slack是國外軟件,國內使用稍微比較慢,內國可以使用仿slack的產品:紛云(https://lesschat.com )。
* 4,CodeReview
CodeReview也是減少程序bug的一個手段,CodeReview是指團隊成員之間互相審核代碼,CodeReview能讓我們發現程序因為粗心導致的低級問題或者代碼性能的問題。有些問題是做人工測試發現不了的。
比如有人在for循環里面查詢數據庫,這是性能極其低的寫法,只有新手才這么干,這樣會導致每次訪問程序都可能查詢幾十次數據庫, 這幾十次查詢其實可以合并成一條用in查詢的SQL語句,這樣只一次查詢就可以。而這種問題只從產品功能來看是看不出問題的,不看代碼是發現不了。
CodeReview還能讓團隊每個人都了解整個程序,避免出現“這程序不是我寫的,我改不了”的情況。
有的人很難靜下心來看別人的代碼,那證明還處于實現階段還沒有進入借鑒階段,可以用本書前面說的分析代碼的方法去看別人的代碼。
如果我們開發前做好了分析和設計的xmind,那么看代碼之前先看看xmind上面列的程序處理邏輯,這樣再去看代碼就比較容易了。
每次CodeReview看新增或修改的代碼即可,以前看過的代碼不用再看,不用每次都從頭看起。可以用版本控制工具對比功能來做CodeReview ,SVN用命令:`svn diff` , GIT用命令:`git difftool`, 具體svn或git的用法大家在網上搜索更多資料。
以上四種方法能有效的保證項目的質量,我們要寫好單元測試,但有很難做到單元測試覆蓋率100%,要確保主要功能做了單元測試,有些功能無法用自動化程序測試的就人工測試,做好CodeReview可以發現人工測試發現不了的問題,還有做好報警系統,這樣即使用戶觸發了我們沒有測試到的bug,我們能及時知道并及時修復。
另外還想提醒大家:好的團隊需要時間。不可強求團隊馬上就能把這些流程和工具用好。