代码整洁之道.md 11.0 KB
Newer Older
S
star 已提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241
:bulb: ***Later equals never.***



#### 1.有意义的命名

软件中随处可见命名,我们给变量、函数、类和包命名,下面是起一个好名字应该遵从的规则。



==名副其实==

选个好名字要花时间,但是省下来的时间比花掉的多,注意命名,而且一旦发现有更好的命名就换掉旧的,这样做,读代码的人都会更开心。

1. 指明计量单位和计量单位的名称,如:int elapsedTimeInDays、int fileAgeInDays 等。

2. 选择体现业务本意的名称,而不是字母(如:d、e),类型(如:theList、list1)等名称。

3. 利用类代替复杂的数据结构。

   例如:应用中使用 int\[]\[] 数组表示坐标,如果在代码中大量使用这个二维数组,代码会变得极其繁杂,我们可以定义一个类

   ~~~java
   public class Position {
       private int value;
       private int x;
       private int y;
   }
   ~~~

4. 避免使用魔术变量(未经定义的常量),例如:

   ~~~java
   if(age == 5){
       // do something
   }
   ~~~

   此时,我们应当将 5 这个魔术数放到文件前面并定义一个数字,如:`private static final int MIN_AGE = 5;`



==避免误导==

程序员必须避免留下掩藏代码本意的错误线索,避免使用与本意相悖的词。

1. 避免使用某些专有名称作为变量名,例如:aix、sco、bat 等,虽然看起来像是不错的缩写,但是极易引起误导。
2. 不要使用 accountList 来表示一组账号,除非它真的是 List 类型,否则会引起错误的判断,此时使用 accountGroup 或 accounts 都是更好的选择。
3. 不要使用外形相似度较高的名称,例如 XYZControllerForEfficientHandlingOfStrings 和 XYZControllerForEfficientStorageOfStrings,更有甚者使用小写的字母 l 和大写的字母 O,他们看起来完全像常量「壹」和「零」。



==做有意义的区分==

在代码中,同一作用范围的两样不同的东西不能重名,如果我们只是为满足编译器或解释器的需要而修改代码,那么将会带来无尽麻烦。

1. 只是在命名后添加数字远远不够,每个不同的命名都应该能表达出不同的意义,如 a1、a2、a3、a4... 等名称纯属误导,完全没有提供正确的信息,例:

   ~~~java
   public void copyChars(char[] c1, char[] c2) {
       for (i = 0; i< c1.length; i++) {
           c2[i] = c1[i];
       }
   }
   ~~~

   此时,两个参数名 c1 和 c2 完全没有任何意义,如果将其替换为 source 和 destination,函数就会好看许多。

2. 避免使用废话来区分命名,假设有一个 Product 类,还有一个 ProductData 和 ProductInfo 类,那么它们虽然名称不同,但是都是意义含混的废话;当我们面对 getActiveAccount、getActiveAccounts 和 getActiveAccountInfo 这 3 个函数时,也无法快速的分辨到底该调用哪一个函数。

3. 避免冗余的命名,例如 variable 永远不要出现在变量名中,table 永远不要出现在数据库表名中,nameString 也不会比 name 更好。



==使用读得出来的名称==

人类擅长于记忆和使用单词,如果名称读不出来,在讨论的时候就会变得十分尴尬。

1. 不要使用字母组合命名,例如程序里面有一个 genymdhms 函数(生成年月日时分秒),这样的函数名完全无法读出来也无法一眼看出它的含义,如果换成 genTimestamp 这样的命名就会使函数读起来更像人话。

2. 不要使用不恰当的缩写,例如:DtaRcrd、Cstm、Acct 并不会比 DataRecord、Customer、Account 这些完整的单词更好更简洁。

   

==使用可搜索的名称==

对于单字母名称和数字常量,有一个问题就是很难再一大段代码中搜索出来。

1. 搜索 MAX_CLASSES_PER_STUDENT 很容易,但是查找数字 7 就比较麻烦了,同时字母 e、s 等也不是一个便于搜索的好名称,我们应该尽量避免使用单字母名称和数字常量。
2. 名称长短应该与其作用域大小相对应,如果变量或常量可能在代码中多处使用,则应该赋予它们便于搜索的名称。



==避免使用标记==

现代编程语言有丰富的类型系统,并且强制使用类型,所以把类型和作用域编进名称里面,只会突然增加负担。

1. 不必使用 m_ 来标记成员变量,我们要学会无视前缀或后缀,只看到名称中有意义的部分。
2. 在创建接口时,不要使用前导字母 I,比如 IShapeFactory 对于 ShapeFactory 来说,前面的 I 字母根本就是一句废话。



==类名和方法名==

1. 类名和对象名应该是名称和名词短语,例如 Customer、Account 等,此外还要避免使用 Data、Info、Manager 这样语义广泛的名称。
2. 方法名应当是动词或动词短语,如 postPayment、deletePage 等。



==一词一义==

1. 每个概念值对应一个词,假如一堆代码中的控制器有 controller、manager 和 driver,name就会让人产生困惑,我们的命名应当一以贯之。
2. 避免将同一单词用于不同目的(双关),例如:使用 add 用于向集合中添加元素,而在另外的地方用来拼接字符串,这种做法是强烈禁止的。



==使用解决方案领域名称和问题领域名称==

1. 只有程序员才会读你的代码,所以我们可以尽情的使用那些计算机科学术语,算法名,模式名,没有程序员不知道 AccoutVisitor 的含义,同样,大多数程序员也都了解 JobQueue 的含义。
2. 如果不能使用程序员熟悉的术语进行命名,那么可以考虑从所涉及的问题的领域获取命名,与软件的实际功能更为贴近。



==为代码添加语境==

很少有名称是能够自我说明的,如果不能自我说明,我们还剩最后一招,给名称添加前缀语境。

1. 当我们看见一堆单词如 state、city、street、name、houseNumber 时,我们很容易推断这是一个地址的描述,但是如果我们只看见一个孤零零的 name 变量呢,我们无法进行推断,但是我们可以添加前缀进行说明:addrName,这样我们就可以明白这个变量是更大结构的一部分。
2. 当需要使用到一堆相关联的变量时,可以使用一个类封装它们,类也是语境的一部分。
3. 不要添加无意义的语境,假如有一个名为 Gas Station Deluxe(加油站豪华版)的应用,如果给其中的每一个类都加上 GSD 前缀就不是什么好点子了;只要名称足够清楚,短名称就比长名称好。



---

#### 2.函数

在编程的早期岁月,系统由程序和子程序组成,后来到了 Fortran 和 PL/1 的年代,系统由程序、子程序和函数组成,如今只有函数保留了下来,函数是所有程序中的第一组代码。



==短小==

函数的第一条规则是短小,第二条规则还是要短小。

1. 函数尽量不要写得过长,**20** 行封顶最佳。

   > :gear: 个人不倾向于这这么短的函数,频繁的函数调用不仅不会降低代码编写难度,还会提高难度。一般情况下,**50-60** 行左右封顶为最佳。

2. if、else、while 等语句,其中的代码应该只占一行,该行一般为一个函数调用语句,这样能够很好的保持函数大小。

3. 函数中的代码嵌套层级(if、while 等嵌套)不宜过多,否则不易于阅读和理解,最好是一层或两层,不可超过三层。



==只做一件事==

函数应该只做一件事,做好这件事,只做一件事。

如何判断函数只做了一件事,还是做了几件事?例:

~~~java
public void buy() {
    // 1.走进商店
    // 2.选好商品
    // 3.掏出钱包
    // 4.付钱
    // 5.放回钱包
    // 6.走出商店
}
~~~

在这个函数中,模拟了一个买东西的情节,其中 1、2、6 步骤都是买东西过程下的抽象层中的一件事,但是 3、4、5 却是在买东西这个动作下的更细分的步骤,很明显不属于函数名下的抽象层,此时 3、4、5 就合一被拆出一个新函数「结账」。

> :yellow_heart: 向下原则:让每个函数后面跟着位于下一抽象层及的函数,在阅读时就能遵循抽象层级向下阅读。





==使用具有描述性的名称==

函数起一个具有描述性的名称,函数的功能越集中,就越便于起一个好名字。

要害怕长名称,长名称要比短而费解的短名称好。

同一个模块中,函数的命名方式要保持一致,使用与模块名一脉相承的短语、名词和动词给函数命名。



==函数参数==

理想的函数参数是 0,其次是 1,再次是 2,应当尽量避免 3 参数的函数(除非万不得已)。

1. 一般来说,信息通过参数输入函数,然后通过返回值进行输出,所以尽量不要在参数中输出函数结果。

2. 不要使用标识参数:向函数输入 boolean 值是一种不优雅的做法,相当于大声宣布本函数不只做一件事,在任何情况下我们都不要这样做;此时我们应该将这种情况拆分为 2 个函数 doXxxForXxx 和 doXxxForNonXxx。

3. 双参数函数:

   1. 两个参数最好是某个值的有序组成部分,如:`new Point(0, 0);`。
   2. 使用两个同类型的参数时,应额外注意其先后顺序,如:`assertEquals(expected, actual);`中一般约定期望值在前,实际值在后。
   3. 尽量的转化双参数函数为单参数函数,可以使用诸如添加成员变量、新建一个类等方法。

4. 三参数函数:如果一个函数看起来需要 2 个、3 个及 3 个以上的参数,说明一些参数应该封装成类了,例:

   ~~~java
   Circle makeCircle(double x, double y, double radius);
   Circle makeCircle(Point center, double radius);
   ~~~

5. 可变参数:在 Java 中,可以使用 ... 向函数传入可变长度的参数。

6. 函数和参数应当形成一种非常良好的动词/名词的对应关系,例如: `writeField(name);`。



==分割指令和询问==

1. 函数要么做什么事,要么回答什么事,两者不可得兼,例:

   ~~~java
   boolean isSuccess = setAttribute("username", "unclebob");
   ---------------------------------------------------------
   if(attributeExists("username")) {
       setAttribute("username", "unclebob");
   }
   ~~~

   此两种代码明显后者更优。

2. 使用异常替代返回错误码,将询问以异常的形式抛出,能够很好的分割指令和询问。
   1. 将 try/catch 代码块单独抽离乘一个函数,使之从主代码块中分离出来,代码就会变得更简洁。
   2. 错误处理就是一件事,处理错误的函数不应该做其他的事,如果在抛出错误的时候需要做另外的一件事,那么应该好好考虑此处是否应该抛出错误。
   3. 尽量使用异常代替错误码,返回错误码一般是一个枚举类,其他许多类都要依赖并使用它,使得在修改这个枚举类时造成了很大的负面压力,所以,尽量使用异常派生类。



---

#### 3.注释