《构建之法》
读书笔记
娄雨禛 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] = ''; 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 != ''); 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 == ' ') 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 倍。
这个结果比第一次优化的结果好了不少,但没有达到本次代码优化的最终目的——在不损失性能的前提下进行感官上阅读体验的提升。当然,我也已经知晓了代码变慢的原因——函数嵌套和函数调用花去了太多无用的时间,接下来的优化步骤也明晰了。