哈工大软件构造阅读心得4-1: 代码评审
本文参考:MIT Reading4哈工大学长汉化
注:这个系列是本人看过阅读资料之后,对看过的内容进行的总结
每一个变量只有一个目的
在 dayOfYear 这个例子中, dayOfMonth被用来做不同意义的值:一开始它是这个月的第几天,最后它是返回的结果(是今年的第几天)。
不要重利用参数,也不要重利用变量。在现在的计算机中,变量不是一个稀缺的资源。当你需要的时候就声明一个(命名一个易理解的名字),不需要它的时候就停止使用。如果你的变量在前面几行代表一个意思,在后面又代表另一个意思,你的读者会很困惑的。
另外,这不仅仅是一个易理解的问题,它也和我们的“远离bug”以及“可改动性”有关。
特别地,方法的参数不应该被修改(这和“易改动性”相关——在未来如果这个方法的某一部分想知道参数传进来的初始值,那么你就不应该在半路修改它)。所以应该使用final关键词修饰参数(这样Java编译器就会对它进行静态检查,防止重引用),然后在方法内部声明其他的变量使用。
难闻的例子#2
在 dayOfYear
中有一个bug——它没有正确处理闰年。为了修复它,我们写了一个判断闰年的方法:
public static boolean leap(int y) {
String tmp = String.valueOf(y);
if (tmp.charAt(2) == '1' || tmp.charAt(2) == '3' || tmp.charAt(2) == 5 || tmp.charAt(2) == '7' || tmp.charAt(2) == '9') {
if (tmp.charAt(3)=='2'||tmp.charAt(3)=='6') return true; /*R1*/
else
return false; /*R2*/
}else{
if (tmp.charAt(2) == '0' && tmp.charAt(3) == '0') {
return false; /*R3*/
}
if (tmp.charAt(3)=='0'||tmp.charAt(3)=='4'||tmp.charAt(3)=='8')return true; /*R4*/
}
return false; /*R5*/
}
注:charAt:Returns the char value at the specified index. An index ranges from 0 to length() - 1. The first char value of the sequence is at index 0, the next at index 1, and so on, as for array indexing.
阅读
2016
当你判断2016年时会发生什么:
leap(2016)
[x] 在 R1处返回true
[ ] 在 R2处返回false
[ ] 在 R3处返回false
[ ] 在 R4处返回true
[ ] 在 R5处返回false
[ ] 在程序运行前报错
[ ] 在程序运行时报错
2017
当你判断2017年时会发生什么:
leap(2017)
[ ] 在 R1处返回true
[x] 在 R2处返回false
[ ] 在 R3处返回false
[ ] 在 R4处返回true
[ ] 在 R5处返回false
[ ] 在程序运行前报错
[ ] 在程序运行时报错
2050
当你判断2050年时会发生什么:
leap(2050)
[ ] 在 R1处返回true
[x] 在 R2处返回false
[ ] 在 R3处返回false
[ ] 在 R4处返回true
[ ] 在 R5处返回false
[ ] 在程序运行前报错
[ ] 在程序运行时报错
10016
当你判断10016年时会发生什么:
leap(10016)
[ ] 在 R1处返回true
[ ] 在 R2处返回false
[ ] 在 R3处返回false
[ ] 在 R4处返回true
[x] 在 R5处返回false
[ ] 在程序运行前报错
[ ] 在程序运行时报错
916
当你判断916年时会发生什么:
leap(916)
[ ] 在 R1处返回true
[ ] 在 R2处返回false
[ ] 在 R3处返回false
[ ] 在 R4处返回true
[ ] 在 R5处返回false
[ ] 在程序运行前报错
[x] 在程序运行时报错
幻数
在这个方法了幻数一共出现了几次(重复的也按多次算)?
12
DRY
假设你写了一个帮助方法:
public static boolean isDivisibleBy(int number, int factor) {
return number % factor == 0;
}
接着 leap() 使用这个 isDivisibleBy(year, …)方法重写,并且正确的使用 leap year algorithm中描述的算法,这时该方法中会出现几个幻数?
3
使用好的名称
好的方法名和变量名都是比较长而且能自我解释的。这种时候注释通常都不必要,因为名字就已经解释了它的用途。
通常来说, tmp, temp, 和 data
这样变量名是很糟糕的(最懒的程序员的标志)。每一个局部变量都是暂时的(temporary),每一个变量也都是数据(data)。所以这些命名都是无意义的。我们应该使用更长、更有描述性的命名。
每一种语言都有它自己的命名传统。
在Java中:
methodsAreNamedWithCamelCaseLikeThis (方法)
variablesAreAlsoCamelCase(变量,注:驼峰命名法)
CONSTANTS_ARE_IN_ALL_CAPS_WITH_UNDERSCORES (常量)
ClassesAreCapitalized (类)
packages.are.lowercase.and.separated.by.dots (包)
ALL_CAPS_WITH_UNDERSCORES 是用来表示 static final这样的常量,所有在方法内部声明的方法,包括用final修饰的,都使用camelCaseNames.
方法的名字通常都是动词,例如 getDate 或者 isUpperCase,而变量和类的名字通常都是名词。尽量选用简洁的命名,但是要避免缩写。
练习
更好的方法名
public static boolean leap(int y) {
String tmp = String.valueOf(y);
if (tmp.charAt(2) == '1' || tmp.charAt(2) == '3' || tmp.charAt(2) == 5 || tmp.charAt(2) == '7' || tmp.charAt(2) == '9') {
if (tmp.charAt(3)=='2'||tmp.charAt(3)=='6') return true;
else
return false;
}else{
if (tmp.charAt(2) == '0' && tmp.charAt(3) == '0') {
return false;
}
if (tmp.charAt(3)=='0'||tmp.charAt(3)=='4'||tmp.charAt(3)=='8')return true;
}
return false;
}
下面哪一个方法名比 leap这个名字 更合适?
[ ] leap
[x] isLeapYear
[ ] IsLeapYear
[ ] is_divisible_by_4
下面哪一个变量名比 tmp 更合适?
[ ] leapYearString
[x] yearString
[ ] temp
[ ] secondsPerDay
[ ] s
使用空白符
在代码行中添加一些一致的空格有利于人们的阅读。leap这个例子就将很多代码“杂糅”在一起——记得加一些空格。
另外要注意的是,永远不要使用Tab字符(注:即\t)来进行缩进,只能使用空格字符。这里强调的是不要使用\t字符,不是说键盘上的这个按键(注:很多编辑器和IDE都会自动把Tab按键作为设置好几个连续的空格输入)。因为不同的工具在显示\t字符的时候长度不一样,有的是8个空格,有的是4个空格,有的是2个空格,所以在你用“git diff”或者其他的编辑器看同一份代码很可能就会显示的不一样。永远将你用的文本编辑器设置为按下Tab键输入空格而非\t。
难闻的例子#3
下面是本次阅读的第三个例子,它呈现了我们剩下要讲的要点:
不要使用全局变量
在Java中,全局变量被声明为 public static 。public修饰符代表它可以从任何地方访问,而 static代表这个变量只会有一个实例化的值。
然而,如果我们加上另一个关键词final : public static final,并且这个变量的类型是不可更改的,那么这个对象就变成了一个“全局常量”。
长期占用内存
全局变量生命周期长,程序运行期一直存在,始终占有那块存储区。
难以定位修改
全局变量是公共的,全部函数都可以访问,难以定位全局变量在哪里被修改,加大了调试的难度。
使函数难以理解
使用全局变量的函数,需要关注全局变量的值,增加了理解的难度,增加了耦合性。
初始化顺序
全局变量的初始化顺序不定,如果全局变量之间有依赖,有可能导致某些变量初始化失败呢,引起莫名其妙bug。
污染命名空间
全局变量会污染命名空间,在函数中局部变量会覆盖全局的值,会出现同一个变量名多个值的情况,造成困惑。
增加耦合性
修改全部变量会影响所有用到它的模块,不利于调试。
在快照图当中的各种变量
在我们画快照图的时候,区别不同种类的变量是很重要的:
-
方法里面的局部变量
-
一个实例化对象中的实例化变量
-
一个类中的静态变量
当方法被调用的时候,局部变量产生,当方法返回时,局部变量消失。如果一个方法被多次同时调用(例如递归),这些方法里面的局部变量互相独立,彼此不会影响。
当一个对象用new实例化后,对象中实例化的变量产生,当这个对象被垃圾回收时,这个变量消失。每一个实例化对象都有它自己的实例化变量。
当程序启动时(更准确点说是包含该静态变量的类被加载的时候),静态变量就产生了,它会一直存活到程序结束。
下面这个例子中使用到了上面三种变量:
下面这个快照图描述了各个变量之间的区别:
局部变量p和args显示在一个栈帧中,它们在main函数被调用的时候动态生成,main函数返回时它们也会跟着消失。而println是在main函数调用它的时候生成的。
实例化变量 value 会在每一个Payment类型的对象中出现。
静态变量 taxRate出现在Payment类型的对象之外,因为它是属于Payment这个类的。任何数量的 Payment类型的对象都可以被创建或销毁(同时它们含有的实例化变量 value也会跟着一起创建和销毁),但是在整个程序中有且仅有一个Payment类,所以这里也有且仅有一个Payment.taxRate变量。 System.out是另一个在这段代码中使用到的静态变量,所以在快照图中也将它显示出来了。
练习
辨识全局变量
在上面第三个例子中,哪一些是全局变量?
[ ] countLongWords
[ ] n
[x] LONG_WORD_LENGTH
[x] longestWord
[ ] word
[ ] words
final的效果
使用final关键词是避开使用全局变量风险的一个办法。如果我们在第三个例子中分别对以下变量使用final关键词会发生什么?
n -> 成为常量
LONG_WORD_LENGTH -> 成为常量
longestWord -> 程序运行前报错
word -> 程序运行前报错
words -> 成为常量
方法应该返回结果,而不是打印它
System.out.意味着如果你想在另一个地方使用它,其中结果可能会做其他的用途,例如参与运算而不是显示出来,程序就得重写。
唯一的例外是debug的时候,你需要将一些关键值打印出来。但是这一部分代码不会是你设计的一部分,只有在debug的时候才能出现。