《構建之法》
讀書筆記
婁雨禛 PB16060356
?
第一部分 關于結對編程的體悟與實踐
在結對編程這一部分我曾講過很多的注意點,比如代碼變量命名風格、縮進風格、注釋風格,前后語句次序風格,等等。然而這里還有一些新的東西。代碼風格這個老掉牙的話題咱們先擱置不談,而說說在結對編程中同樣重要的其他注意點。
?
代碼復審
代碼復審是一門學問。良好而有序的復審將幫助我們快速排查問題和增進代碼可讀性,而低劣的復審則純粹在浪費時間。這里拿我在個人項目中的代碼舉一個例子。
?
1 void anothermain(char *fileString) 2 { 3 char *offset = fileString; 4 char move; 5 char temp; 6 7 int count; 8 do 9 { 10 if(temp = *offset) 11 { 12 if(temp >= 32 && temp <= 126) 13 { 14 Characters++; 15 if(temp >= 65 && temp <= 122) 16 { 17 if(temp >= 91 && temp <= 96) 18 { 19 offset++; 20 continue; 21 } 22 else 23 { 24 offset++; 25 word[0] = temp; 26 } 27 } 28 else if(temp >= 49 && temp <= 57) 29 { 30 offset++; 31 while(*offset >= 49 && *offset <= 57) 32 { 33 Characters++; 34 offset++; 35 } 36 while((*offset >= 49 && *offset <= 57) || (*offset >= 65 && *offset <= 90) || (*offset >= 97 && *offset <= 122)) 37 { 38 Characters++; 39 offset++; 40 } 41 continue; 42 } 43 else 44 { 45 offset++; 46 continue; 47 } 48 } 49 else 50 { 51 if(*offset == 10) 52 enterNum++; 53 offset++; 54 continue; 55 } 56 } 57 else 58 break; 59 if(temp = *offset) 60 { 61 if(temp >= 32 && temp <= 126) 62 { 63 Characters++; 64 if(temp >= 65 && temp <= 122) 65 { 66 if(temp >= 91 && temp <= 96) 67 { 68 offset++; 69 continue; 70 } 71 else 72 { 73 offset++; 74 word[1] = temp; 75 } 76 } 77 else if(temp >= 49 && temp <= 57) 78 { 79 offset++; 80 while(*offset >= 49 && *offset <= 57) 81 { 82 Characters++; 83 offset++; 84 } 85 while((*offset >= 49 && *offset <= 57) || (*offset >= 65 && *offset <= 90) || (*offset >= 97 && *offset <= 122)) 86 { 87 Characters++; 88 offset++; 89 } 90 continue; 91 } 92 else 93 { 94 95 offset++; 96 continue; 97 } 98 } 99 else 100 { 101 if(*offset == 10) 102 enterNum++; 103 offset++; 104 continue; 105 } 106 } 107 else 108 break; 109 if(temp = *offset) 110 { 111 if(temp >= 32 && temp <= 126) 112 { 113 Characters++; 114 if(temp >= 65 && temp <= 122) 115 { 116 if(temp >= 91 && temp <= 96) 117 { 118 offset++; 119 continue; 120 } 121 else 122 { 123 offset++; 124 word[2] = temp; 125 } 126 } 127 else if(temp >= 49 && temp <= 57) 128 { 129 offset++; 130 while(*offset >= 49 && *offset <= 57) 131 { 132 Characters++; 133 offset++; 134 } 135 while((*offset >= 49 && *offset <= 57) || (*offset >= 65 && *offset <= 90) || (*offset >= 97 && *offset <= 122)) 136 { 137 Characters++; 138 offset++; 139 } 140 continue; 141 } 142 else 143 { 144 offset++; 145 continue; 146 } 147 } 148 else 149 { 150 if(*offset == 10) 151 enterNum++; 152 offset++; 153 continue; 154 } 155 } 156 else 157 break; 158 159 if(temp = *offset) 160 { 161 if(temp >= 32 && temp <= 126) 162 { 163 Characters++; 164 if(temp >= 65 && temp <= 122) 165 { 166 if(temp >= 91 && temp <= 96) 167 { 168 offset++; 169 continue; 170 } 171 else 172 { 173 offset++; 174 word[3] = temp; 175 for(count = 4; isLetterOrNum(*offset); offset++, count++) 176 { 177 Characters++; 178 word[count] = *offset; 179 } 180 word[count] = '\0'; 181 NumOfWords++; 182 SolveTheWord(word); 183 } 184 } 185 else if(temp >= 49 && temp <= 57) 186 { 187 offset++; 188 while(*offset >= 49 && *offset <= 57) 189 { 190 Characters++; 191 offset++; 192 } 193 while((*offset >= 49 && *offset <= 57) || (*offset >= 65 && *offset <= 90) || (*offset >= 97 && *offset <= 122)) 194 { 195 Characters++; 196 offset++; 197 } 198 continue; 199 } 200 201 else 202 { 203 offset++; 204 continue; 205 } 206 } 207 else 208 { 209 if(*offset == 10) 210 enterNum++; 211 offset++; 212 continue; 213 } 214 } 215 else 216 break; 217 218 219 } 220 while(*offset != '\0'); 221 }
?
這是我在進行匆忙的數據結構轉型時書寫的非常潦草的代碼。雖然潦草,但還是包含了我的很多想法,比如,進肯能利用“流式操作”減少機器的循環流程,提高代碼效率,盡管這樣會使代碼變得非常難讀。很快,我就意識到了,這樣的操作雖然節省了機器執行的時間,卻大大浪費了我自己的時間。這段代碼的可讀性非常的差,以至于我自己常常被繞暈。其中雜亂的命名方式就不多說了,簡直不忍卒讀。由于是一次個人作業,又到了馬上要提交的截止日期,我只關心這段程序的執行結果是否正常(作用是判斷是否為一個單詞并進行儲存),因此在最后輸出結果正確之下,我就沒有再對它進行優化和修改。
而如果這換成一次結對編程,將會怎樣呢?
是的,代碼復審就登場了。那么下面,我就結合這個例子,簡要說說應該怎樣利用復審,改進代碼。
?
Step1:“自己改+你說我看”:不符合共同協定的編碼風格的地方,一律修改
我們需要在一開始就商定好代碼風格,比如哪些地方需要換行,變量的命名規則等等。
然后,先過自己這一關——自己對代碼進行修改。比如在上面的例子中,函數名為anothermain,這是什么鬼意思?這種根本不知其所以然的命名,必須根除。還有像enternum這樣的變量,也會讓人摸不著頭腦,應該改為characterNum這種規范的命名。
在自己認為沒有大毛病之后,還需要對方對自己進行挑錯。在這里,挑錯要注意一個原則:不要在雞蛋里挑骨頭,要本著找出有意義的錯誤的原則,認真查錯。比如,“這樣的函數命名會不會和以后的命名沖突,從而引起歧義?”
Step2:探討潛在的邏輯問題,預防“邏輯災害”的發生
上面這段代碼的邏輯完全是給機器看的,對于人類閱讀者來說,幾乎看不下去。這時候,雖然程序在執行的時候結果沒有問題,代碼還是得改。把從頭執行到尾的“流式操作”改為稍微慢一些卻容易閱讀地多的代碼,我們可以增加一些執行很小功能的函數,然后反復調用它們。這樣,將得到容易閱讀得多的代碼。
Step3:修改完后也不松懈,繼續進行經驗總結
總結這個步驟,是讓我們“事半功倍”的不二捷徑。它不容忽視
?
在修改之后,代碼變成了下面這樣。
1 void WordCheck(FILE *fp) 2 { 3 bool stop = false; 4 bool isEmpty = false; 5 char ch; // ch gets the character one by one 6 short check = 0; // to check if the first four characters are letters 7 short i = 0; 8 9 ch = fgetc(fp); 10 if(ch == EOF) 11 isEmpty = true; 12 13 for(; !stop; ch = fgetc(fp)) // if it is not the end of the text 14 { 15 if(ch >= 32 && ch <= 126) 16 characterNum++; 17 if(ch == '\n') 18 lineNum++; 19 20 if(check < 4) // to check the first four characters 21 { 22 if(ch == EOF) 23 { 24 stop = true; 25 if(isEmpty == false) 26 lineNum++; 27 } 28 29 else if(IsLetter(ch) == true) 30 { 31 ++check; 32 tempWord[i] = ch; 33 ++i; // search for the next 34 } 35 else 36 { 37 i = 0; 38 check = 0; 39 ClearTemp(); 40 } 41 } 42 else // first four characters are all letters, ready to store 43 { 44 if(IsSeparator(ch) || ch == EOF) // have met a separator, store the word 45 { 46 i = 0; // roll back to the beginning in the next search 47 check = 0; // roll back to the beginning in the next search 48 49 wordNum++; // have found another word 50 StoreWord(); // store the word 51 ClearTemp(); // prepare for the next search 52 53 if(ch == EOF) 54 { 55 stop = true; 56 if(isEmpty == false) 57 lineNum++; 58 } 59 } 60 61 else // have not met a separator, keep searching 62 { 63 tempWord[i] = ch; 64 ++i; // search for the next 65 } 66 } 67 } 68 }
?
根據運行結果顯示,修改后的代碼運行時間是原來的兩倍,但我們卻獲得了清爽得多的函數。這應該是一個好的結果,至少在對速度的要求不高的情況下。
然而,如果我們進行過程序優化,就會發現,這是一個很令人詫異的結果——代碼的運行時間變成了原來的兩倍!這究竟是怎么回事呢?
原來,在新的函數中,我采取了反復調用子函數的措施來精簡代碼。而在我的子函數中,我為了繼續精簡代碼,又套用的新的子函數,這里進行了兩次的套用。而在判斷是否為一個單詞并進行儲存的時候,這種層層調用的次數是超乎想象的。正是這里頭的反復嵌套,大大拖慢了程序的運行速度。
接下來是進行修改,在不損失代碼簡潔性的前提下進行代碼優化,其中主要是性能優化。我首先要做的是把兩層的函數嵌套改為一層。
注意,在這里我并沒有把子函數放上來,是出于閱讀的簡潔性和直觀性——直接把我的實踐結論告訴大家,而不是用代碼和大家繼續兜圈子。
通過將兩層的函數嵌套改為一層,我的子函數從原來的16行變成了42行,可以說復雜了不少,但接下來我們看一下運行效率。
代碼的運行時間大約是最初運行時間的 1.4 倍。
這個結果比第一次優化的結果好了不少,但沒有達到本次代碼優化的最終目的——在不損失性能的前提下進行感官上閱讀體驗的提升。當然,我也已經知曉了代碼變慢的原因——函數嵌套和函數調用花去了太多無用的時間,接下來的優化步驟也明晰了。
?