1. 程式人生 > >讀書報告之《修改程式碼的藝術》 (II)

讀書報告之《修改程式碼的藝術》 (II)

今天繼續從本書的第二章學習,

昨天我們已經總結了下面三個內容

1. 降低修改的風險

2. 需要修改大量相同的程式碼

3. 時間緊迫,必須修改


今天繼續第四點

4. 修改時應當測試哪些方法

作者提出了影響結構圖的概念。說穿了,就是CallRelation和ReferenceRelation,就是檢視某個方法(變數)被哪些方法引用,以及自身又引用了哪些方法,依次類推。這個複雜的關係網實際就是一顆風險評估樹(圖)。通過這棵樹,我們可以知道某個修改會影響到哪些節點。這項引數,既是風險的直接量化指標,同時又是驗證修改的測試指標。這是很樸素的思想,不管有意無意,你肯定已經在這麼做了,不要告訴我你的boss從來沒問過你這樣的問題:“這個修改有風險嗎?”。

5. 多處修改是否將所有相關類解依賴

實際還是上面的問題,上面的風險評估樹(圖),不同的節點,地位是不一樣的。有些節點就顯得特別關鍵,其中,作者歸納了一種叫匯點的概念。匯點就是所有的修改的影響都能從這個點感知,所以只要針對這個點測試就足夠了,沒必要滿大街的測試了。打個比方就是醫學上所謂的全息部位,人的耳朵就是人身上的一個全息點,身體的其他部位有任何毛病,都能在耳朵上看出來。

所以測試時,只要在匯點這個地方解開依賴就可以了。

6. 修改巨型方法

作者舉例了兩種常見的巨型方法樣式

  • Ø  列表型。一般是長長的switchcase或者很多的處理步驟。
  • Ø  鋸齒型。一般是複雜的ifelse多重巢狀。這種程式碼很難提煉方法,需要小心。

對列表型的巨型方法,其邏輯總體還是清晰的,可以通過將每一個列表項提煉出單獨的方法,從而將巨型方法瘦身。

對鋸齒型的巨型方法,其邏輯一般來說就有點反人類了。原書中沒給出什麼有意義的內容,這裡就以自己的切身體會舉幾個具體的例子。

在正式舉例之前,先引用一些大牛的作品, 以免顯得井底之蛙。《重構》一書中的關於簡化條件表示式的一章,絕對不容錯過。然後《effective java》一書中也有關於if else的簡化。然後是這篇博文if/else的使用心得。這些都是非常基礎的內容,看似直白,其實卻是代數中的交換律、結合律、分配律,是大智若愚,是各種技巧的基礎。


化簡複雜的if else語句,基本的手段就兩個:

  1. 針對頭重腳輕的if else,使用return快速返回,從而減少巢狀層數。
  2. 合併分支。有些分支的執行內容相同,往往意味著可以合併為一個分支
  3. 扁平化。基本原理如下

  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)這個分支也做同樣的簡化,於是程式碼就變成這個樣子

	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";
		
		}
	}

最後, else if (score < 70 && score >= 60) 這個條件也是有化簡的餘地 .因為只有在if (score < 60)不成立時,才會判斷這條語句,這就意味著score >=60必然成立,所以可以簡化為else if (score < 70
 
 
	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++程式設計規範》一書中的一節內容:

不要進行不成熟的優化。 優化的第一原則:不要優化。 第二原則(僅適用於專家,不是磚家哦):還是不要優化。再三測試,而後優化。

正確、簡單和清晰第一。簡單就是美:正確優於速度;簡單優於複雜;清晰由於技巧;安全優於不安全

為了這點效能,有必要把程式寫成這樣嗎?難道就沒有更好的方法,顯然不是了