讀書報告之《修改程式碼的藝術》 (II)
今天繼續從本書的第二章學習,
昨天我們已經總結了下面三個內容
1. 降低修改的風險
2. 需要修改大量相同的程式碼
3. 時間緊迫,必須修改
今天繼續第四點
4. 修改時應當測試哪些方法
作者提出了影響結構圖的概念。說穿了,就是CallRelation和ReferenceRelation,就是檢視某個方法(變數)被哪些方法引用,以及自身又引用了哪些方法,依次類推。這個複雜的關係網實際就是一顆風險評估樹(圖)。通過這棵樹,我們可以知道某個修改會影響到哪些節點。這項引數,既是風險的直接量化指標,同時又是驗證修改的測試指標。這是很樸素的思想,不管有意無意,你肯定已經在這麼做了,不要告訴我你的boss從來沒問過你這樣的問題:“這個修改有風險嗎?”。
5. 多處修改是否將所有相關類解依賴
實際還是上面的問題,上面的風險評估樹(圖),不同的節點,地位是不一樣的。有些節點就顯得特別關鍵,其中,作者歸納了一種叫匯點的概念。匯點就是所有的修改的影響都能從這個點感知,所以只要針對這個點測試就足夠了,沒必要滿大街的測試了。打個比方就是醫學上所謂的全息部位,人的耳朵就是人身上的一個全息點,身體的其他部位有任何毛病,都能在耳朵上看出來。
所以測試時,只要在匯點這個地方解開依賴就可以了。
6. 修改巨型方法
作者舉例了兩種常見的巨型方法樣式
- Ø 列表型。一般是長長的switchcase或者很多的處理步驟。
- Ø 鋸齒型。一般是複雜的ifelse多重巢狀。這種程式碼很難提煉方法,需要小心。
對列表型的巨型方法,其邏輯總體還是清晰的,可以通過將每一個列表項提煉出單獨的方法,從而將巨型方法瘦身。
對鋸齒型的巨型方法,其邏輯一般來說就有點反人類了。原書中沒給出什麼有意義的內容,這裡就以自己的切身體會舉幾個具體的例子。
在正式舉例之前,先引用一些大牛的作品, 以免顯得井底之蛙。《重構》一書中的關於簡化條件表示式的一章,絕對不容錯過。然後《effective java》一書中也有關於if else的簡化。然後是這篇博文if/else的使用心得。這些都是非常基礎的內容,看似直白,其實卻是代數中的交換律、結合律、分配律,是大智若愚,是各種技巧的基礎。
化簡複雜的if else語句,基本的手段就兩個:
- 針對頭重腳輕的if else,使用return快速返回,從而減少巢狀層數。
- 合併分支。有些分支的執行內容相同,往往意味著可以合併為一個分支
- 扁平化。基本原理如下
if (expr1) {
if (expr2) {
// do something 1
} else {
// do something 2
}
} else {
if (expr3) {
// do something 3
} else {
// do something 4
}
}
必然等價於
if (expr1 && expr2) {
// do something 1
} else if (expr1 && !expr2) {
// do something 2
} else if (expr3) {
// do something 3
} else {
// do something 4
}
好了,嘮嗑就到這。下面第一個例子,這是一個按分數計算成績等級的方法,也就是分為A,B, C, D, E五個等級。好有熟悉感啊
/**
* 按成績排等級:90分以上A, 80-90 B, 70-80 C, 60-70 D, 60以下 E
* @param score
* @return string of degree, such as A, B, C, D, E
*/
String degree(int score) {
if (score < 80) {
if (score < 70) {
if (score < 60) {
return "E";
} else {
return "D";
}
} else {
return "C";
}
} else {
if (score < 90) {
return "B";
} else {
return "A";
}
}
}
這個例子是從網上隨便搜的一個例子,在改動之前不得不吐槽一下(好像每次拿到別人寫得程式碼時總是不自覺的會這樣 ) : 這麼簡單的程式也能寫得如此複雜,實在不知道是該誇你還是貶你?
採用扁平化策略,先化簡if (score < 80)這個分支。邏輯條件很容易出錯,這裡建議亦步亦趨的做。先按照扁平化的公式,做一個形式上的變形。
String degree(int score) { if (score < 80 && score < 70) { if (score < 70) { if (score < 60) { return "E"; } else { return "D"; } } else { return "C"; } } else if (score < 80 && !(score < 70)){ if (score < 70) { if (score < 60) { return "E"; } else { return "D"; } } else { return "C"; } } else {
這裡我不急著化簡 if (score < 80 && score < 70)中的邏輯條件,也不急著處理 if (score < 80 && score < 70)分支中的內容。 同樣,我也不急著處理新增的 else if (score < 80 && !(score < 70))分支,而且這個分支的內容我也只是複製原始分支中的內容。記住,這樣做是絕對等價的,因而到目前為止的改動,雖然程式碼變化很大,但實際上是相當安全的。
接下來,化簡if (score < 80 && score < 70) ==> if (score < 70), 同時化簡這個分支中的執行程式碼,實際只要去掉永遠不會被執行的語句就可以了
if(score < 80&& score < 70) {==> if(score< 70)
if (score < 70) {// 這個條件已經沒必要了if (score < 60) {
return"E";
}else{
return"D";
}
}else{
return"C";// 這個分支永遠不會被執行......
}對 else if (score < 80 && !(score < 70)這個分支也做同樣的簡化,於是程式碼就變成這個樣子
最後, else if (score < 70 && score >= 60) 這個條件也是有化簡的餘地 .因為只有在if (score < 60)不成立時,才會判斷這條語句,這就意味著score >=60必然成立,所以可以簡化為else if (score < 70 )繼續扁平化,最終就變成String degree(int score) { if (score < 70) { if (score < 60) { return "E"; } else { return "D"; } } else if (score < 80 && score >= 70){ return "C"; } else { if (score < 90) { return "B"; } else { return "A"; } } }
String degree(int score) { if (score < 60) { return "E"; } else if (score < 70 && score >= 60) { return "D"; } else if (score < 80 && score >=70){ return "C"; } else if (score < 90){ return "B"; } else { return "A"; } }
String degree(int score) {
if (score < 60) {
return "E";
} else if (score < 70) {
return "D";
} else if (score < 80){
return "C";
} else if (score < 90){
return "B";
} else {
return "A";
}
}
以上是最終程式碼,與開始的程式碼比較,一切的言語和解釋都是多餘的
最後說一下,這個例子的原始程式碼 作者願意是為了效能優化,因為一般情況下,成績分佈總是80分以下佔大部分,所以這樣寫成這樣,可以減少條件判斷的次數。簡單說來就是借用一下霍夫曼編碼的思想。這裡我只能呵呵了, 直接摘錄《C++程式設計規範》一書中的一節內容:
不要進行不成熟的優化。 優化的第一原則:不要優化。 第二原則(僅適用於專家,不是磚家哦):還是不要優化。再三測試,而後優化。
正確、簡單和清晰第一。簡單就是美:正確優於速度;簡單優於複雜;清晰由於技巧;安全優於不安全
為了這點效能,有必要把程式寫成這樣嗎?難道就沒有更好的方法,顯然不是了