前言
什麼是註釋?
在程式語言中,註釋就是對程式碼的解釋和說明,其目的是讓人們能夠更加輕鬆地瞭解程式碼。
也有一句話是這樣說的:程式設計師都討厭兩件事,1.別人不寫註釋 2.自己寫註釋
在開發者社群裡,我不止一次的看到吐槽離職的前同事不寫註釋的例子,其實不光是他人的程式碼,即使是自己寫的程式碼,一段時間以後再去看,你也會發現:這寫的什麼呀?
作為開發者,我們大多都知道編寫註釋的重要性,但是卻往往抱著"能實現功能就可以了"這樣的心態去寫程式碼,完全隨心所欲的去實現,結果當然就是留下一堆爛攤子,形成前文所述的惡性迴圈,所以在編寫程式碼的同時,請牢記程式碼首先是寫給人看的。
編寫精煉的、準確的註釋 只需要幾秒鐘,但是以後可能節省其他人幾個小時 的時間來讀懂您的程式碼。
寫好註釋
寫註釋簡單嗎?簡單。那麼寫好註釋,簡單嗎?
答案就跟寫文章一樣,有的文章是記流水賬,有的則是發人深省,回味無窮。
註釋也是同樣,在《程式碼精進之路》裡總結了編寫註釋的三項原則:
- 準確,錯誤的註釋比沒有註釋更糟糕。
- 必要,多餘的註釋浪費閱讀者的時間。
- 清晰,混亂的註釋會把程式碼搞得更亂。
比如,當我們說程式語言時,一定不要省略“程式設計”這兩個字。否則,就可能被誤解為大家日常說話用的自然語言。這就是準確性的要求。
bad:
String language = "Java"; // the language
複製程式碼
better:
String language = "Java"; // the programming language
複製程式碼
如果程式碼已經能夠清晰、簡單地表達自己的語義和邏輯,這時候重複程式碼語義的註釋就是多餘的註釋。註釋的維護是耗費時間和精力的,所以,不要保留多餘的、不必要的註釋。還有一句很精闢的話:Code tells you How, Comments tell you Why.
bad:
// the programming language
String programmingLanguage = "Java";
複製程式碼
better:
String programmingLanguage = "Java";
複製程式碼
如果註釋和程式碼不能從視覺上清晰地分割,註釋就會破壞程式碼的可讀性。
bad:
/* dump debug information
if (hasDebug) {
System.out.println("Programming language: Jave");
} */
String programmingLanguage = "Java";
複製程式碼
better:
// dump debug information
//
// if (hasDebug) {
// System.out.println("Programming language: Jave");
// }
String programmingLanguage = "Java";
複製程式碼
《Effective Dart》中關於如何寫註釋,也推薦要清晰和準確,同時還有簡潔。
在運用Dart語言編寫Flutter應用的過程中,由於檢視層都是純Dart程式碼,而且夾雜著許多的if..else..
,需要根據條件顯示不同的widget,因此在做好widget拆分的同時,編寫良好的註釋勢在必行。
Effective Dart 文件註釋
在學習並使用Flutter框架開發App的過程中,有些開發者並沒有怎麼關注Dart語言,草草看了幾下語法之後就開始了Flutter之旅,還是按照以前的Java、Swift這樣的語法風格進行開發,這在以後可能會帶來不必要的麻煩,比如團隊成員裡各個都不同的命名方式、註釋也各不相同等等。
因此推薦先看一下 《Effective Dart》,主要內容包含程式碼風格、文件註釋、最佳實踐、設計指南,能從中獲益良多。我們主要關心文件註釋這一章節,這裡我總結了以下兩點:
-
使用
///
放棄/** ... */
在《Effective Dart》解釋了相應原因
由於歷史原因,dartdoc 支援兩種格式的文件註釋:
///
(“C# 格式”) 和/** ... */
(“JavaDoc 格式”)。我們推薦使用///
是因為其更加簡潔。/**
和*/
在多行註釋中間新增了開頭和結尾的兩行多餘 內容。///
在一些情況下也更加易於閱讀,例如 當註釋文件中包含有使用*
標記的列表內容的時候。如果你現在還在使用 JavaDoc 風格格式,請考慮 使用新的格式。
與此同時,要使用 ///
文件註釋來註釋成員和型別。
bad:
// The number of characters in this chunk when unsplit.
int get length => ...
複製程式碼
better:
/// The number of characters in this chunk when unsplit.
int get length => ..
複製程式碼
而//
則主要用於方法體內的註釋
greet(name) {
// Assume we have a valid name.
print('Hi, $name!');
}
複製程式碼
-
把第一個語句定義為一個段落並使用散文的方式來描述
註釋文件中的第一個段落應該是簡潔的、面向使用者的註釋。例如下面的示例, 通常不是一個完成的語句。
bad:
/// Starts a new block as a child of the current chunk. Nested blocks are
/// handled using their own independent [LineWriter].
ChunkBuilder startBlock() { ... }
複製程式碼
better:
/// Defines a flag.
///
/// Throws an [ArgumentError] if there is already an option named [name] or
/// there is already an option using abbreviation [abbr]. Returns the new flag.
Flag addFlag(String name, String abbr) { ... }
複製程式碼
這就跟日常寫部落格一樣,會先寫一個段落大意,然後圍繞著這點進行詳細描述,這樣更容易讓讀者理解核心思想,也更加節省時間。
另外推薦使用散文的方式來描述引數、返回值以及異常資訊。
在其他語言中,比如JavaDoc
使用各種標籤和額外的註釋來描述引數和 返回值。
bad:
/// Defines a flag with the given name and abbreviation.
///
/// @param name The name of the flag.
/// @param abbr The abbreviation for the flag.
/// @returns The new flag.
/// @throws ArgumentError If there is already an option with
/// the given name or abbreviation.
Flag addFlag(String name, String abbr) { ... }
複製程式碼
而 Dart 把引數、返回值等描述放到文件註釋中,並使用方括號來引用 以及高亮這些引數和返回值。
better:
/// Defines a flag.
///
/// Throws an [ArgumentError] if there is already an option named [name] or
/// there is already an option using abbreviation [abbr]. Returns the new flag.
Flag addFlag(String name, String abbr) { ... }
複製程式碼
我們主要注意這兩點,其它規範可以檢視《文件註釋》。
在mvvm_flutter專案中的實踐
mvvm_flutter是我在Flutter中運用MVVM架構的一個示例。
mvvm_flutter : github.com/ditclear/mv…
專案完成後只簡單的在幾個關鍵方法上新增了幾個JavaDoc
格式的註釋,比如:
/**
* call the model layer 's method to login
* doOnData : handle response when success
* doOnError : handle error when failure
* doOnListen : show loading when listen start
* doOnDone : hide loading when complete
*/
Observable login() => _repo
.login(username, password)
.doOnData((r) => response = r.toString())
.doOnError((e, stacktrace) {
if (e is DioError) {
response = e.response.data.toString();
}
})
.doOnListen(() => loading = true)
.doOnDone(() => loading = false);
複製程式碼
現在我們開始將其轉換為Dart語言推薦的註釋風格。如下所示:
/// 登入
///
/// 呼叫 model層[GithubRepo] 的 login 方法進行登入
/// 傳入 [username] 和 [password]
/// 成功:顯示返回的資訊
/// 失敗:處理錯誤,顯示錯誤資訊
/// 訂閱開始:loading = true
/// 訂閱結束:loading = false
/// 返回 [Observable] 給 View 層
Observable login() => _repo
.login(username, password)
.doOnData((r) => response = r.toString())
.doOnError((e, stacktrace) {
if (e is DioError) {
response = e.response.data.toString();
}
})
.doOnListen(() => loading = true)
.doOnDone(() => loading = false);
複製程式碼
相比之前的程式碼,清晰乾淨了蠻多,我們在註釋的第一行中描述了這個方法的功能,隨後以散文的方式對方法的呼叫、引數以及返回值進行了描述。
這是ViewModel層的註釋,接下來我們來進行Model層的註釋。
class GithubRepo {
/// ...
Observable login(String username, String password) {
_sp.putString(KEY_TOKEN, "basic " + base64Encode(utf8.encode('$username:$password')));
return _remote.login();
}
}
複製程式碼
我們對login
方法進行註釋,結果如下:
/// 倉庫層
class GithubRepo {
/// 登入
///
/// 將ViewModel層 傳遞過來的[username] 和 [password] 處理為 token 並用[_sp]進行快取
/// 呼叫 [_remote] 的 [login] 方法進行網路訪問
/// 返回 [Observable] 給ViewModel層
Observable login(String username, String password) {
_sp.putString(KEY_TOKEN, "basic " + base64Encode(utf8.encode('$username:$password')));
return _remote.login();
}
}
複製程式碼
這裡方法比較簡單,但對於複雜的倉庫層的方法,可能包含著網路、資料庫、MethodChannel
、SharedPreferences
等等互動,運用Dart推薦的註釋方式,可以更好的描述你的程式碼邏輯。
/// 獲取文章詳情
///
/// 1.先通過 資料庫[_local] 檢視本地是否有 id 為 [articleId]的文章
/// 2.有快取則到第4步,沒有快取則到第3步
/// 3.通過網路層[_remote] 獲取服務端資料,成功後再進行快取 ,到第4步
/// 4.返回 [Observable] 給ViewModel層
Observable getArticleDetail(int articleId) {
return _local
.getArticleById(articleId)
.onErrorResumeNext(
_remote.getArticleById(articleId)
.doOnData((article) => _local.insertArticle(article)));
}
複製程式碼
這樣,我們輕鬆的拆解了程式碼邏輯,也能更容易的編寫出相應的測試用例,方便進行單元測試。
而View
層相比Model
層和ViewModel
層,需要額外注意的是其夾雜著邏輯判斷,需要根據條件顯示不同的Widget
,我們在將複雜部分提取成方法的時候,也需要對其進行詳細的描述。
/// 登入按鈕內部的widget
///
/// 當請求進行時 [value.loading] 為 true 時,顯示 [CircularProgressIndicator]
/// 否則顯示普通的登入文字
Widget buildLoginChild(HomeProvide value) {
if (value.loading) {
return const CircularProgressIndicator();
} else {
return const FittedBox(
fit: BoxFit.scaleDown,
child: const Text(
'Login With Github Account',
maxLines: 1,
textAlign: TextAlign.center,
overflow: TextOverflow.fade,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0, color: Colors.white),
),
);
}
}
複製程式碼
經過這番改進,當其他開發者閱讀您的程式碼時,也能減少很多不必要的煩惱。
寫在最後
在運用《Effective Dart》中的註釋技巧進行文件註釋時,會有一種在寫部落格的感覺,因為寫部落格的好處之一便是備忘,將來再複習的時候能夠更加快速的理解知識點,註釋也是這樣。
我認為即使是再優秀的開發者,如果不寫註釋,時間長了,也會忘記,而且有幸見識過一個Java檔案包含了30000+行程式碼,還沒有註釋,到現在都沒有人願意去接手這樣的專案,大家都是程式設計師,程式設計師何苦為難程式設計師呢。
參考資料:
==================== 分割線 ======================
如果你想了解更多關於MVVM、Flutter、響應式程式設計方面的知識,歡迎關注我。
你可以在以下地方找到我:
簡書:www.jianshu.com/u/117f1cf0c…
Github: github.com/ditclear