こんにちは。コミュニケーションアプリ「LINE」のモバイルクライアントを開発している石川です。
この記事は、毎週木曜の定期連載 “Weekly Report” 共有の第 26 回です。Weekly Report については、第 1 回の記事を参照してください。
説明の計は一文目にあり
クラスや関数が複雑な時や、直感的でない時は、ドキュメンテーションコメントを書くことで理解を助けることができます。
以下のドキュメンテーションは、String
を受け取り、 List<List<String>>
を返す関数の動作を説明しています。しかし、このドキュメンテーションが分かりやすいとは言い難いです。どのように改善することができるでしょうか?
/**
* 与えられた [englishText] をピリオド (`'.'`)で分割し、分割された文字列から空文字列 (`""`) を除去します。
* 次に、各分割された文字列を再度スペース (`' '`) またはカンマ (`','`) で分割した上で、空のリストを除去します。
* 最後に、その結果 (文字列のネストされたリスト) を戻り値として返します。
*/
fun ...(englishText: String): List<List<String>> {
val sentences = englishText
.split(SENTENCE_SEPARATOR)
.asSequence()
val wordsInSentences = sentences
.map { it.split(WORD_SEPARATOR_REGEX).filter(String::isNotEmpty) }
return wordsInSentences
.filter(List<String>::isNotEmpty)
.toList()
}
...
private val SENTENCE_SEPARATOR: String = "."
private val WORD_SEPARATOR_REGEX: Regex = """[ ,]+""".toRegex()
始めが肝心
ドキュメンテーションは、1 文目を読んだだけで概要を理解できるように書かれているべきです。しかし、先程のドキュメンテーションはそうなっておらず、説明を最後まで読まないと挙動を理解することはできません。
1 文目だけで理解できるようなドキュメンテーションを書くためには、以下の 2 点に気をつければよいでしょう。
- 最も重要な要素を選ぶ。
- コードよりも高い抽象度で説明する。
この方針に従って、先程のドキュメンテーションを改善してみましょう。まずは、元のドキュメンテーションに書かれていた要素を列挙してみます。
- 文字列
englishText
が与えられる - ピリオドで文字列を分割する
- 空の文字列を除去する
- スペースまたはカンマで分割する
- 空のリストを除去する
- 文字列のネストされたリストを返す
この中から、まずは最も重要な要素を選びます。この関数の目的は戻り値を得ることなので、「文字列のネストされたリストを返す」が最も重要だと考えられます。
「文字列のネストされたリスト」の抽象度は、コードと同程度になっています。次に、この「ネストされたリスト」の意味を考えてみましょう。外側のリストは englishText
をピリオドによる分割していることから「英文」を示し、内側のリストはスペースやカンマによる分割に対応することから「英単語」を示していると言えます。これらのことから、ドキュメンテーションの最初の文は以下のように書けます。
/**
* 英文の文字列 [englishText] を文ごとに分割し、さらに単語ごとに分割して得られた、リストのリストを返します。
「何の文字で分割するか」や「どのようなものを除外するか」といった詳細は、最初の重要なポイントの後に追加します。
* ここで、「文」はピリオド (`'.'`) で区切られた部分文字列を意味し、
* 「単語」はスペース (`' '`) かカンマ (`','`) で区切られた部分文字列を意味します。
* また、戻り値からは、空の文や空の単語は除外されます。
もし、多くのコーナーケースやエッジケースがある場合は、引数と戻り値の例を示すのも良いでしょう。
* 例えば、`" a bc. .d,,."` が与えられた場合は、`[["a", "bc"], ["d"]]` を返します。
*/
ドキュメンテーション全体としては以下のようになります。
/**
* 英文の文字列 [englishText] を文ごとに分割し、さらに単語ごとに分割して得られた、リストのリストを返します。
*
* ここで、「文」はピリオド (`'.'`) で区切られた部分文字列を意味し、
* 「単語」はスペース (`' '`) かカンマ (`','`) で区切られた部分文字列を意味します。
* また、戻り値からは、空の文や空の単語は除外されます。
* 例えば、`" a bc. .d,,."` が与えられた場合は、`[["a", "bc"], ["d"]]` を返します。
*/
このようにすることで、最初の一文だけを読むだけで概要が分かり、より詳細を知りたければ続きの文を読めば分かるというドキュメンテーションになります。
ドキュメンテーション以外にも
ドキュメンテーション以外のコメント(インラインコメントなど)でも、「最初に何を説明するか」は重要です。例えば、以下の「ワークアラウンドコード(問題回避のためのコード)」の説明には、改善の余地があります。
val someValue = device.someValue
device.someFunction()
...
// 強制的に `someValue` を以前の値にリセットします。
// デバイス X では `someFunction` が呼ばれると `someValue` の値を変更するのですが、
// この動作は foo API の仕様に違反しているからです。
device.someValue = someValue
上記のコメントは最初に「何をしているか」を説明していますが、「何をしているか」はワークアラウンドコードにとって、それほど重要ではありません。この場合、存在理由の方を先に書いたほうが良いでしょう。
// これは、デバイス X 固有のバグを回避するためのコードです。
// デバイス X は `someFunction` 内で `someValue` を変更しますが、これは foo API の仕様に違反します。
device.someValue = someValue
別の例として、TODO コメントについても取り上げます。TODO コメントでは、「今後どうしたいか」や「どのような状態が理想か」が最も重要になることが多いです。それらを先に書き、「現状が良くない理由」や「なぜすぐに改善できないか」などは後ろに書きましょう。以下の 2 つの TODO コメントでは、後者のほうがよりよい書き方です。
// TODO: `var abc` と `var xyz` は両方とも `fun foo` に代入されるため、
// `foo` をリファクタリングするまでは変更を加えにくい。
// 本来は、`abc` は `xyz` から計算で求められるため、削除できる。
var abc :...
var xyz :...
// TODO: `var abc` の値を `var xyz` から求めるようにし、`abc` を削除する。
// ただしそのためには、`fun foo` のリファクタリングを先に行う必要がある。
// (`foo` が `abc` と `xyz` に代入するため。)
var abc :...
var xyz :...
一言まとめ
コメントを書くときは、最初に説明する内容を注意深く選ぶ。
キーワード: comment
, documentation
, short summary