麻省理工18年春软件构造课程阅读04“代码评审”
本文內容來自MIT_6.031_sp18: Software Construction課程的Readings部分,采用CC BY-SA 4.0協議。
由于我們學校(哈工大)大二軟件構造課程的大部分素材取自此,也是推薦的閱讀材料之一,于是打算做一些翻譯工作,自己學習的同時也能幫到一些懶得看英文的朋友。另外,該課程的閱讀資料中有許多練習題,但是沒有標準答案,所給出的答案均為譯者所寫,有錯誤的地方還請指出。
譯者:李秋豪
審校:
V1.0 Thu Mar 8 22:58:41 CST 2018
本次課程的目標
- 代碼評審:閱讀并討論別人寫的代碼。
- 好代碼的書寫原則:你在代碼評審的過程中應遵循的標準,不管編程目的或編程語言是什么。
代碼評審
代碼評審是一種系統的對別人代碼的研究,和論文審校很類似。
代碼評審有兩個主要目的:
- 提升代碼質量。 找出存在及潛在的bug,分析代碼的清晰度以及代碼是否嚴格遵循了當前工程的標準。
- 提升程序員的水平。 代碼評審是提升程序員水平的一個重要方法,通過它可以學習到語言新的特性、工程上新的設計以及一些新的實現方法。特別是在開源項目中,很多交流都是在代碼評審這種環境下進行的。
代碼評審已經在開源項目中運用很深了,例如Apache 和 Mozilla. 同樣的,代碼評審在工業界也應用很廣,在Google,如果你的代碼沒有另一個程序員的評審簽字,你是沒辦法將它提交的。
在本課程中,我們會在“Problem sets”環節上進行一系列的代碼評審,詳細的信息可參考 Code Reviewing document 。(譯者注:這是MIT要求學生之間相互進行代碼評審并打分。我們學校沒有進行這項活動)
風格標準
大多數公司或者大的項目都會要求代碼風格具有統一的標準。這些標準可能會非常細化,例如縮進應該是幾個空格,花括號應該怎么對齊。這些問題上的爭論通常會導致 神圣的戰爭 (譯者注:例如vim和emacs哪一個更好),畢竟它們關乎于個人的口味或者審美觀。
在本門課程中,我們對代碼風格沒有一個統一的要求。如果你是剛開始寫Java,我們推薦你遵循 Google Java Style ,它在工業界運用的很廣,可讀性也不錯,例如:
if (isOdd(n)) { n = 3*n + 1; }- 在關鍵詞(if)后面留空格,但是在函數調用(isOdd)后不留空格
- 在行的末尾寫{ ,而 } 自己單獨一行
- 無論是空塊還是只有一行,都要用{…}包括起來
不過,我們不會要求你遵循花括號的放置風格,畢竟每個程序員都有自己的口味。但是要注意,一旦遵循某一種風格后就要一直這樣寫,不要一會這樣一會那樣。同時,應該優先遵守所在項目規定的風格,如果你在進行代碼評審的時候擅自改動別人的代碼風格,你的搭檔會恨死你的;)總之,團隊合作優先。
同時,有一些代碼風格是跟我們這門課程的三個目標息息相關的(譯者注:遠離bug、易讀性、可改動性),它們可不止花括號放在哪這么簡單。這篇閱讀的剩下部分將探討這些規則,而你在進行代碼評審或是自己寫代碼的時候也應該注意這些規則。但是,代碼評審可不僅僅是看別人的代碼風格,我們在后續的課程還會講到很多別的事情,例如規格說明、抽象數據類型、并發編程和線程安全等等,這些都是代碼評審的原材料。
難聞的(Smelly)例子 #1
程序員通常會將差代碼描述為“難聞的”(bad smell)?!按a衛生”(Code hygiene)則是另一個描述這方面的詞?,F在讓我從一個“難聞的”代碼開始吧:
public static int dayOfYear(int month, int dayOfMonth, int year) {if (month == 2) {dayOfMonth += 31;} else if (month == 3) {dayOfMonth += 59;} else if (month == 4) {dayOfMonth += 90;} else if (month == 5) {dayOfMonth += 31 + 28 + 31 + 30;} else if (month == 6) {dayOfMonth += 31 + 28 + 31 + 30 + 31;} else if (month == 7) {dayOfMonth += 31 + 28 + 31 + 30 + 31 + 30;} else if (month == 8) {dayOfMonth += 31 + 28 + 31 + 30 + 31 + 30 + 31;} else if (month == 9) {dayOfMonth += 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31;} else if (month == 10) {dayOfMonth += 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30;} else if (month == 11) {dayOfMonth += 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31;} else if (month == 12) {dayOfMonth += 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 31;}return dayOfMonth; }接下來的幾節都會圍繞這一代碼段展開。
別寫重復的代碼(Don’t Repeat Yourself)
重復的代碼很不安全。如果你在兩個地方放置了相似的代碼,那么一個最基本的風險就是如果一處出現了bug,另一處也非??赡苡衎ug。而修復的時候經常只會修復一個地方而忽略了另一個地方。
避免重復就像你過馬路的時候要避免被車撞一樣。賦值-粘貼在編程中是一個很大的誘惑,而你在使用它的時候,“皮膚應該感覺到危險而震顫”。(譯者注:這描述也是醉了)
別重復代碼(Don’t Repeat Yourself,)簡稱為DRY,現在已經成為了編程人員的一句咒語。
譯者注(來自維基百科):
一次且僅一次(once and only once,簡稱OAOO)又稱為Don't repeat yourself(不要重復你自己,簡稱DRY)或一個規則,實現一次(one rule, one place)是面向對象編程中的基本原則,程序員的行事準則。旨在軟件開發中,減少重復的信息。
DRY的原則是──系統中的每一部分,都必須有一個單一的、明確的、權威的代表──指的是(由人編寫而非機器生成的)代碼和測試所構成的系統,必須能夠表達所應表達的內容,但是不能含有任何重復代碼。當DRY原則被成功應用時,一個系統中任何單個元素的修改都不需要與其邏輯無關的其他元素發生改變。此外,與之邏輯上相關的其他元素的變化均為可預見的、均勻的,并如此保持同步。
其起源已經不可考,一般認為這個原則最初由Andy Hunt和Dave Thomas在他們的書The Pragmatic Programmer中提出。因為極限編程方法的創始者之一肯特·貝克總結和宣傳而使其廣為人知。
違反DRY原則的解決方案通常被稱為WET,其有多種全稱,包括“write everything twice”(在每個地方寫兩次)、“we enjoy typing”(我們就是喜歡打字)或“waste everyone's time”(浪費大家的時間)。
上面 dayOfYear 這個例子充滿了重復的代碼,你能夠試著將它們修復嗎?
閱讀小練習
在 dayOfYear() 有一種重復是數值的重復,請問在 dayOfYear() 一共出現了幾次四月份的天數?
9
正如上面所提到的,重復的代碼會給修復帶來麻煩,如果我們的日歷講二月份改為30天而不是28天,這段代碼一共要修改幾處?
10
另一種重復是代碼 dayOfMonth+=的重復。假設你建立了一個數組:int[] monthLengths = new int[] { 31, 28, 31, 30, ..., 31} ,以下哪一種語句架構能夠幫助你DRY,使得 dayOfMonth+= 僅出現一次呢?
- [x] for (int m = 1; m < month; ++m) { ... }
- [ ] switch (month) { case 1: ...; break; case 2: ...; break; ... }
- [ ] while (m < month) { ...; m += 1; }
- [ ] if (month == 1) { ... } else { ... dayOfYear(month-1, dayOfMonth, year) ... }
僅在需要的地方注釋
一個好的開發者應該在代碼中明智的添加注釋。好的注釋會使得代碼易于修改,遠離bug(因為一些重要的設想已經寫出來了),并且減小了改動的難度。
一種重要的注釋就是規格說明,通常出現在方法或者類的前部,一般會描述出類或方法的行為、參數、返回值、用法/例子等等。在Java中,規格說明通常按照Javadoc的標準來寫:以 /** 開始,中間用 @-標出參數和返回值,最后以*/結尾。例如:
/*** Compute the hailstone sequence.* See http://en.wikipedia.org/wiki/Collatz_conjecture#Statement_of_the_problem* @param n starting number of sequence; requires n > 0.* @return the hailstone sequence starting at n and ending with 1.* For example, hailstone(3)=[3,10,5,16,8,4,2,1].*/ public static List<Integer> hailstoneSequence(int n) {... }另一種重要的注釋就是標出是從哪引用的別的代碼。這在實際編程中是非常重要的,當你從別的網站上引用代碼的時候。同時,本門課程的要求 6.031 collaboration policy 也是這樣規定的。例如:
// read a web page into a string // see http://stackoverflow.com/questions/4328711/read-url-to-string-in-few-lines-of-java-code String mitHomepage = new Scanner(new URL("http://www.mit.edu").openStream(), "UTF-8").useDelimiter("\\A").next();其中的一個原因就是避免版權糾紛。你在Stack Overflow上引用的代碼可能是用的公共版權協議,但是在別處的代碼就未必了。另一個原因在于很多網站上的代碼可能已經“過期”了,它可能不在符合現有的語言標準或者有更好的解決方案。例如這個回答就已經不適合現在的Java寫法了。
有一些注釋是不必要的。例如直接將代碼行為翻譯為英語(好像讀者完全不懂Java一樣):
while (n != 1) { // test whether n is 1 (don't write comments like this!)++i; // increment il.add(n); // add n to l }但是不易理解的代碼應該被注釋(例如實現一些特定的算法):
int sum = n*(n+1)/2; // Gauss's formula for the sum of 1...n// here we're using the sin x ~= x approximation, which works for very small x double moonDiameterInMeters = moonDistanceInMeters * apparentAngleInRadians;閱讀小練習
僅在需要的地方注釋
下面哪一些注釋是合理的?(獨立思考每一個注釋,就當其它注釋不存在一樣)
/** @param month month of the year, where January=1 and December=12 [C1] */ public static int dayOfYear(int month, int dayOfMonth, int year) {if (month == 2) { // we're in February [C2]dayOfMonth += 31; // add in the days of January that already passed [C3]} else if (month == 3) {dayOfMonth += 59; // month is 3 here [C4]} else if (month == 4) {dayOfMonth += 90;}...} else if (month == 12) {dayOfMonth += 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 31;}return dayOfMonth; // the answer [C5] }[x] C1
[ ] C2
[x] C3
[ ] C4
[ ] C5
快速報錯/失敗(Fail-fast)
快速報錯是指代碼應該盡可能快的將其中的bug暴露出來。因為問題暴露的越早(越接近),其修復工作也會越容易。正如我們在第一篇閱讀資料里看到的,靜態檢查比動態檢查更早報錯,動態檢查比產生錯誤的結果(這也可能會影響接下來的計算)更早報錯。
很明顯, dayOfYear 這個函數并沒有快速報錯——如果你輸入一個順序不對的參數,它只會靜悄悄的返回一個錯誤的值。事實上,依照 dayOfYear 參數的設計方法,一個不是美國本土的用戶很可能輸入一個順序不對的參數。所以, dayOfYear 需要靜態或者動態檢查來檢測這種錯誤。
閱讀小練習
快速報錯
public static int dayOfYear(int month, int dayOfMonth, int year) {if (month == 2) {dayOfMonth += 31;} else if (month == 3) {dayOfMonth += 59;} else if (month == 4) {dayOfMonth += 90;} else if (month == 5) {dayOfMonth += 31 + 28 + 31 + 30;} else if (month == 6) {dayOfMonth += 31 + 28 + 31 + 30 + 31;} else if (month == 7) {dayOfMonth += 31 + 28 + 31 + 30 + 31 + 30;} else if (month == 8) {dayOfMonth += 31 + 28 + 31 + 30 + 31 + 30 + 31;} else if (month == 9) {dayOfMonth += 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31;} else if (month == 10) {dayOfMonth += 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30;} else if (month == 11) {dayOfMonth += 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31;} else if (month == 12) {dayOfMonth += 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 31;}return dayOfMonth; }假設現在的日期是2019年二月9號,dayOfYear 返回的正確答案應該是40。以下輸入分別會導致什么結果呢?(靜態錯誤、動態錯誤、不報錯返回正確答案、不報錯返回錯誤的答案)
dayOfYear(2, 9, 2019)不報錯返回正確答案
dayOfYear(1, 9, 2019)不報錯返回錯誤的答案
dayOfYear(9, 2, 2019)不報錯返回錯誤的答案
dayOfYear("February", 9, 2019)靜態錯誤
dayOfYear(2019, 2, 9)不報錯返回錯誤的答案
dayOfYear(2, 2019, 9)不報錯返回錯誤的答案
更快速的報錯
以下哪一種措施會使得我們的報錯更加快速呢?
public static int dayOfYear(String month, int dayOfMonth, int year) { ... }更快報錯——靜態錯誤
public static int dayOfYear(int month, int dayOfMonth, int year) {if (month < 1 || month > 12) {return -1;}... }更快報錯——動態錯誤
public static int dayOfYear(int month, int dayOfMonth, int year) {if (month < 1 || month > 12) {throw new IllegalArgumentException();}... }更快報錯——動態錯誤
public enum Month { JANUARY, FEBRUARY, MARCH, ..., DECEMBER }; public static int dayOfYear(Month month, int dayOfMonth, int year) {... }更快報錯——靜態錯誤
public static int dayOfYear(int month, int dayOfMonth, int year) {if (month == 1) {...} else if (month == 2) {...}...} else if (month == 12) {...} else {throw new IllegalArgumentException("month out of range");} }更快報錯——動態錯誤
避免幻數
有一個笑話說的是計算機科學家只認識1和0這兩個數字,有時候加上一個2.(譯者注:好冷。。他的意思是盡量不要經常在代碼中寫1和0以外的常數)
除了這幾個數以外的常數都被稱為“ 幻數”,因為它們就好像不知道從哪突然冒出來一樣。
解決幻數的一個辦法就是寫注釋,但是另一個更好的辦法是聲明一個具有合理名字的變量。
上面的dayOfYear 就充滿了幻數:
- 月份2,.....,12如果用 FEBRUARY, …, DECEMBER.會更加容易理解
- days-of-months 30, 31, 28等等 如果用存儲在數據結構中的數會更加容易理解,例如列表或者數組e.g. MONTH_LENGTH[month].
- 前面的59和90實際上是程序員自己加起來算出的!它們不僅沒有注釋,而且正確性依賴于程序員算術的正確性! 永遠不要在代碼用硬編碼你自己計算的數值,讓程序去做所有的數值計算工作,例如MONTH_LENGTH[JANUARY] + MONTH_LENGTH[FEBRUARY]即易于理解又不會計算錯誤。
閱讀小練習
避免幻數
在以下代碼中,你覺得2大概代表什么意思?
if (month == 2) { ... }[ ] 2 可能代表一月
[x] 2 可能代表二月
[ ] 2 可能代表五月
[ ] 2 可能代表公元二年
當你要靠猜測的時候,會發生什么
假設你正在閱讀 turtle圖形庫中的一段代碼,你對此并不熟悉:
turtle.rotate(3);僅僅通過這段代碼,你覺得3表達了什么意思?
[ ] 3可能代表順時針3度
[ ] 3可能代表逆時針3度
[x] 3可能代表順時針3弧度
[ ] 3可能代表順時針3圈
用名字而非數字
思考下面這段代碼,它嘗試畫出一個正邊形:
for (int i = 0; i < 5; ++i) {turtle.forward(36);turtle.turn(72); }這些幻數使得這段代碼脫離了我們定下的三個目標:遠離bug、易讀性、易改動性(safe from bugs (SFB), not easy to understand (ETU) and not ready for change (RFC))。
對于下面這些重寫的代碼,你認為它們有哪些改進?(SFB, ETU, and/or RFC三個方面考慮)
final int five = 5; final int thirtySix = 36; final int seventyTwo = 72; for (int i = 0; i < five; ++i) {turtle.forward(thirtySix);turtle.turn(seventyTwo); }[x] 沒有提升(或者變差了)
[ ] 遠離bug
[ ] 易讀性
[ ] 易改動性
[x] 沒有提升(或者變差了)
[ ] 遠離bug
[ ] 易讀性
[ ] 易改動性
[ ] 沒有提升(或者變差了)
[ ] 遠離bug(譯者注:其實這里也有一定的遠離bug,如果把最后畫出來不是一個正邊形當做bug的話。不過這里的bug應該是指for循環中可能會添加修改x的代碼,而x又是循環量)
[ ] 易讀性
[x] 易改動性
[ ] 沒有提升(或者變差了)
[x] 遠離bug
[x] 易讀性
[x] 易改動性
每一個變量有且只有一個目的
在 dayOfYear 這個例子中, dayOfMonth 被用來做不同意義的值:一開始它是這個月的第幾天,最后它是返回的結果(是今年的第幾天)。
不要重利用參數,也不要重利用變量。在現在的計算機中,變量不是一個稀缺的資源。當你需要的時候就聲明一個(命名一個易理解的名字),不需要它的時候就停止使用。如果你的變量在前面幾行代表一個意思,在后面又代表另一個意思,你的讀者會很困惑的。
另外,這不僅僅是一個易理解的問題,它也和我們的“遠離bug”以及“可改動性”有關。
特別地,方法的參數不應該被修改(這和“易改動性”相關——在未來如果這個方法的某一部分想知道參數傳進來的初始值,那么你就不應該在半路修改它)。所以應該使用final關鍵詞修飾參數(這樣Java編譯器就會對它進行靜態檢查,防止重引用),然后在方法內部聲明其他的變量使用。
public static int dayOfYear(final int month, final int dayOfMonth, final int year) {... }“難聞的”例子 #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*/elsereturn 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*/ }這個代碼中有bug嗎?它的代碼風格有什么問題(根據前面說過的)?
閱讀小練習
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
使用好的名稱
好的方法名和變量名都是比較長而且能自我解釋的。這種時候注釋通常都不必要,因為名字就已經解釋了它的用途。
例如,你可以這樣寫:
int tmp = 86400; // tmp is the number of seconds in a day (don't do this!)或這樣寫:
int secondsPerDay = 86400;通常來說, tmp, temp, 和 data 這樣變量名是很糟糕的(最懶的程序員的標志)。每一個局部變量都是暫時的(temporary),每一個變量也都是數據(data)。所以這些命名都是無意義的。我們應該使用更長、更有描述性的命名。
每一種語言都有它自己的命名傳統。在Python中,類通常是大寫的,變量通常是小寫,并且單詞是用“_”來區分開的(words_are_separated_by_underscores)。在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, 而變量和類的名字通常都是名詞。盡量選用簡潔的命名,但是要避免縮寫:例如, message 而不是 msg, word而不是 wd. 因為看你代碼的程序員可能是非英語母語的!這些縮寫可能在他們看來很難懂。
另外要避免使用一個字母當變量的名字,除了在一些傳統上根據能看懂的情況。例如x和y在用于坐標系的時候就很清晰,i和j用于變量的循環變量的時候就很清晰。但是如果你的代碼充斥了像 e, f, g, 和 h這樣的單字母變量,讀者會很難理解它們的用途的。
閱讀小練習
更好的方法名
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;elsereturn 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就是一個典型的反面例子。dayofYear就好的多。事實上, dayOfYear很好的將各個行用縮進進行了分隔,它們開始來很適合人們閱讀。
在代碼行中添加一些一致的空格有利于人們的閱讀。leap這個例子就將很多代碼“雜糅”在一起——記得加一些空格。
另外要注意的是,永遠不要使用Tab字符 (譯者注:即\t)來進行縮進,只能使用空格字符。這里強調的是不要使用\t字符,不是說鍵盤上的這個按鍵(譯者注:很多編輯器和IDE都會自動把Tab按鍵作為設置好幾個連續的空格輸入)。因為不同的工具在顯示\t字符的時候長度不一樣,有的是8個空格,有的是4個空格,有的是2個空格,所以在你用“git diff”或者其他的編輯器看同一份代碼很可能就會顯示的不一樣。永遠將你用的文本編輯器設置為按下Tab鍵輸入空格而非\t 。
“難聞的”例子 #3
下面是本次閱讀的第三個例子,它呈現了我們剩下要講的要點:
public static int LONG_WORD_LENGTH = 5; public static String longestWord;public static void countLongWords(List<String> words) {int n = 0;longestWord = "";for (String word: words) {if (word.length() > LONG_WORD_LENGTH) ++n;if (word.length() > longestWord.length()) longestWord = word;}System.out.println(n); }不要使用全局變量
避免使用全局變量,現在我們把這個詞拆開具體分析:
- “變量”,說明它的值是可以修改的
- “全局的”,說明它可以從程序的任何地方訪問
為什么全局變量是不好的 列出了一系列全局變量的缺點,可以參考一下。
在Java中,全局變量被聲明為 public static 。 public 修飾符代表它可以從任何地方訪問,而 static代表這個變量只會有一個實例化的值。
然而,如果我們加上另一個關鍵詞final : public static final,并且這個變量的類型是不可更改的(immutable,譯者注:參考第二篇閱讀“Java基礎”),那么這個對象就變成了一個“全局常量”。一個全局常量可以在任何位置讀取,但是永遠不會被賦予新的值或對象,所以風險也就沒有了。全局常量是很常見的,而且很有用。
通常來說,我們應該使用參數傳遞和返回值而非全局變量,或者將它們放到你調用的方法的所屬類中。我們會在后面的閱讀中介紹很多這樣的方法。
在快照圖中的各種變量
在我們畫快照圖的時候,區別不同種類的變量是很重要的(譯者注:參考第二篇閱讀“Java基礎”):
- 方法里面的局部變量
- 一個實例化對象中的實例化變量
- 一個類中的靜態變量
當方法被調用的時候,局部變量產生,當方法返回時,局部變量消失。如果一個方法被多次同時調用(例如遞歸),這些方法里面的局部變量互相獨立,彼此不會影響。
當一個對象用new實例化后,對象中實例化的變量產生,當這個對象被垃圾回收時,這個變量消失。每一個實例化對象都有它自己的實例化變量。
當程序啟動時(更準確點說是包含該靜態變量的類被加載的時候),靜態變量就產生了,它會一直存活到程序結束。
下面這個例子中使用到了上面三種變量:
class Payment {public double value;public static double taxRate = 0.05;public static void main(String[] args) {Payment p = new Payment();p.value = 100;taxRate = 0.05;System.out.println(p.value * (1 + taxRate));} }下面這個快照圖描述了各個變量之間的區別:
局部變量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 -> 成為常量
方法應該返回結果,而不是打印它
countLongWords 并不具備可更改性。它最后向控制臺輸出結果, System.out.這意味著如果你想在另一個地方使用它,其中結果可能會做其他的用途,例如參與運算而不是顯示出來,程序就得重寫。
通常來說,只有最高層的代碼才會處理與人/控制臺的交互。唯一的例外是debug的時候,你需要將一些關鍵值打印出來。但是這一部分代碼不會是你設計的一部分,只有在debug的時候才能出現。
總結
代碼評審是一種廣泛應用的軟件質量提升方法。它可以檢測出代碼中的各種問題,但是作為一個初學課程,這篇閱讀材料只提及了下面幾個好代碼通用的原則:
- 不要重復你的代碼(DRY)
- 僅在需要的地方做注釋
- 快速失敗/報錯
- 避免使用幻數
- 一個變量有且僅有一個目的
- 使用好的命名
- 避免使用全局變量
- 返回結果而非打印它
- 使用空白符提升可讀性
下面把今天學的內容和我們的三個目標聯系起來:
- 遠離bug. 通常來說,代碼評審使用人的審查來發現bug。DRY使得你只用在一處地方修復bug,避免bug的遺漏。注釋使得原作者的假設很清晰,避免了別的程序員在更改代碼的時候引入新的bug。快速報錯/失敗使得bug能夠盡早發現,避免程序一直錯更多。避免使用全局變量使得修改bug更容易,因為特定的變量只能在特定的區域修改。
- 易讀性. 對于隱晦或者讓人困惑的bug,代碼評審可能是唯一的發現方法,因為閱讀者需要嘗試理解代碼。使用明智的注釋、避免幻數、變量目的單一化、選擇好的命名、使用空白字符都可以提升代碼的易讀性。
- 可更改性. DRY的代碼更具有可更改性,因為代碼只需要在一處進行更改。返回結果而不是打印它使得代碼更可能被用作新的用途。
轉載于:https://www.cnblogs.com/liqiuhao/p/8531425.html
總結
以上是生活随笔為你收集整理的麻省理工18年春软件构造课程阅读04“代码评审”的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 编程开发之--java多线程学习总结(2
- 下一篇: mysql命令_MySQL常用操作命令