Effective Dart 文件註釋在Flutter專案中的實踐

ditclear發表於2019-04-11

前言

什麼是註釋?

在程式語言中,註釋就是對程式碼的解釋和說明,其目的是讓人們能夠更加輕鬆地瞭解程式碼。

也有一句話是這樣說的:程式設計師都討厭兩件事,1.別人不寫註釋 2.自己寫註釋

在開發者社群裡,我不止一次的看到吐槽離職的前同事不寫註釋的例子,其實不光是他人的程式碼,即使是自己寫的程式碼,一段時間以後再去看,你也會發現:這寫的什麼呀

作為開發者,我們大多都知道編寫註釋的重要性,但是卻往往抱著"能實現功能就可以了"這樣的心態去寫程式碼,完全隨心所欲的去實現,結果當然就是留下一堆爛攤子,形成前文所述的惡性迴圈,所以在編寫程式碼的同時,請牢記程式碼首先是寫給人看的

編寫精煉的、準確的註釋 只需要幾秒鐘,但是以後可能節省其他人幾個小時 的時間來讀懂您的程式碼。

寫好註釋

寫註釋簡單嗎?簡單。那麼寫好註釋,簡單嗎?

答案就跟寫文章一樣,有的文章是記流水賬,有的則是發人深省,回味無窮。

註釋也是同樣,在《程式碼精進之路》裡總結了編寫註釋的三項原則:

  1. 準確,錯誤的註釋比沒有註釋更糟糕。
  2. 必要,多餘的註釋浪費閱讀者的時間。
  3. 清晰,混亂的註釋會把程式碼搞得更亂。

比如,當我們說程式語言時,一定不要省略“程式設計”這兩個字。否則,就可能被誤解為大家日常說話用的自然語言。這就是準確性的要求。

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》,主要內容包含程式碼風格文件註釋最佳實踐設計指南,能從中獲益良多。我們主要關心文件註釋這一章節,這裡我總結了以下兩點:

  1. 使用///放棄/** ... */

《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!');
}
複製程式碼
  1. 把第一個語句定義為一個段落並使用散文的方式來描述

註釋文件中的第一個段落應該是簡潔的、面向使用者的註釋。例如下面的示例, 通常不是一個完成的語句。

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();
  }
}

複製程式碼

這裡方法比較簡單,但對於複雜的倉庫層的方法,可能包含著網路、資料庫、MethodChannelSharedPreferences等等互動,運用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+行程式碼,還沒有註釋,到現在都沒有人願意去接手這樣的專案,大家都是程式設計師,程式設計師何苦為難程式設計師呢。

參考資料:

Code Complete-自說明程式碼

程式碼精進之路-寫好註釋,真的是小菜一碟?

Effective Dart - 文件註釋

==================== 分割線 ======================

如果你想了解更多關於MVVM、Flutter、響應式程式設計方面的知識,歡迎關注我。

你可以在以下地方找到我:

簡書:www.jianshu.com/u/117f1cf0c…

掘金:juejin.im/user/582d60…

Github: github.com/ditclear

Effective Dart 文件註釋在Flutter專案中的實踐

相關文章