コメント書くや書かざるや
3-shake Advent Calendar 2023、20 日目の記事です。
ソフトウェア開発のプログラミングにおける、コメントのあり方と書き方について。
書くや書かざるや
コメントについて議論する時、書くや書かざるや?といったゼロイチ討論になりがちである。しかし本来は「書き方」が提案されて、その中から適した方法を取捨選択するのが望ましい。そのうえで書かないとしても、明確な意思による選択ならコメントなしでも十分な状態になっているはずだ。
以上を踏まえ本記事は「私なりの書き方」を提案し、コメント検討の一助となることを目指す。
書式
プログラミング言語にもよるが、派生の多い C 言語系の構文ではコメントを「ブロック コメント」と「行コメント」に大別する。改行を含む複数行ならブロック、処理の注釈など簡潔なものは行コメントという感じで使い分ける。
/**
* ブロック コメント
*/
int main(int argc, char *argv[]) {
// 行コメント (C99 以降)
printf("Hello, world!\n");
}
ブロック コメントについては更に、Java の Javadoc あたりから構造化されるようになった。そして Java の隆盛もあってか、後続の言語でも同様の書式を採用するものは多い。このような書式を私は構造化コメントと呼んでいる。以下に私が利用したことのあるものをまとめる。
言語 | 記法 | 備考 |
---|---|---|
Java | Javadoc | @tag で構造化。 |
JavaScript | JSDoc | 〃 |
TypeScript | TSDoc | 〃 |
Kotlin | KDoc | 〃 |
Swift | Documentation Comment Syntax | Markdown で構造化。 |
Rust | Doc comments | 〃 |
C# | XML ドキュメント コメント | XML で構造化。 |
プログラミング言語としては他に C++ と Objective-C を経験しているが、これらは標準の構造化コメント書式を持たない。ただしコメント構文は C 言語系なので Javadoc を採用して Doxygen によりドキュメント生成していた。
本記事のサンプルは近年、私がよく利用している TypeScript と TSDoc を選ぶことにする。
場所
コメントを書く場所について、以下を提案する。
- 構造化コメント
- クラス、メソッド (メンバー関数)、関数
- フィールド (メンバー変数)、プロパティー
- 定数
- 行コメント
- 関数内の処理
構造化コメントは主に関数以上のスコープを想定しているため、そこへ書く。API リファレンスを想像するとわかりやすい。
扱いの難しいのが行コメント。悪いコメントの典型例として「処理をそのまま説明する」ものがある。行コメントは関数内を対象とするゆえ、そう書かれがちである。
// 1 と 2 を足した結果
const value = sum(1, 2)
しかし以下の用途なら、行コメントも有効だと考える。
- 意味や目的でグループ化できる処理のラベル
- コードとしても意図的に空行をはさまず、まとめて記述するもの
- まとまった処理の先頭に行コメントを書く
- コメントはグループを総称する名前とする
- 関数が長くなったり複数の処理グループができたら、この行コメントを目印に関数を分割したりする
- 特別な背景を持つ処理の注釈
- 特別な背景により非直感的な処理となった場合、それを注釈する
- いわゆる TODO コメントもこれに含まれる
これらを踏まえたサンプルを書いてみた。
import { fetchUser } from './api'
/**
* ユーザー情報。
*/
type User = {
/** ユーザー識別子。 */
id: string
/** ユーザー名。 */
name: string
/** 詳細な説明。 */
description: string
}
/**
* ユーザー情報を取得します。
* @param id - 取得したいユーザーの識別子。
* @returns ユーザー情報。取得に失敗した場合はエラー情報。
*/
export const getUserInfo = async (id: string): Promise<Result<User>> => {
try {
const user = await fetchUser(id)
if (!user) {
// 設計の問題で API が成功してもデータを返さない場合があるため、明示的に例外とする
throw new Error('ユーザーが見つかりませんでした。')
}
return {
type: 'success',
payload: user,
}
} catch (error) {
return {
type: 'error',
error,
}
}
}
User
型、getUserInfo
は関数以上のスコープなので構造化コメントを採用。関数内では利用している API の設計的な問題と対応を行コメントで注釈している。その他の処理は言語機能と型、命名で十分に説明的なためコメントは書いていない。
文体
書式と異なりコメントの文体は定型がないため、自分で決める必要がある。しかしいざ検討するとなると意外に難しい。
そのため、なにかを手本にすることをオススメする。私の場合は .NET API 公式ドキュメントだった。他には JDK 公式ドキュメントも機械翻訳とあるが、結構よい。これらのドキュメントは一貫した形式と文体で書かれており、日本語訳もある。手本として参考になるはず。
- .NET API ブラウザー | Microsoft Learn
- .NET API ドキュメントのトップ ページ
- String.EndsWith メソッド (System) | Microsoft Learn
- 文体の例として
String
クラスのメソッドを紹介 - ここまで詳細に書く必要はないが、文体と雰囲気は伝わると思う
- 文体の例として
- JDK 21 ドキュメント - ホーム
文体について、もう少し掘り下げたい。型や様式は手本を参考にするとして構成はどうればよいか。私の場合、以下の基準と順番を意識している。
- 目的
- 背景
- 注釈
はじめにコメント対象の目的を書く。関数やメソッドなら命名がそのまま目的となることが多いから、それを自然言語で補強する。getter/setter だと「〜を取得・設定します」と直訳のようになってしまうが、それでもよい。これはこれで、それ以上の目的や意図がないことの証明になる。
つぎに背景。目的だけでは説明しきれない用途や存在理由を書く。例えば「〜として利用されることを想定しています」とか「〜を解決するための一時的な処理です」のような感じで。Javadoc 系の場合 @see
で外部資料の URL を記述して代替する手もある。
最後に注釈。これは目的と背景の補完であったり、特別な注意の必要な処理やデータの解説を書く。
FAQ
記事の締めとして、コメントに関するよくある疑問と私の回答をまとめる。
Q. どれぐらい書けばいい?
推奨は関数以上のスコープすべて。ただし強制ではない。
これはかなりの量で、コストも高く感じられるだろう。しかしコードは書くより読むほうが長く、考える時間は更に長い。そのため読む、考えることの補助としてコメントは十分に見合う。
そのうえで書くタイミングが重要である。コメント対象を実装した時点なら最も理解度が高い。そのためコードの延長としてスラスラ書けるだろう。鉄は熱いうちに叩けである。私はここを逃さないように注意しており、結果としてコストはキーボードを打つ程度と認識している。つまり低い。
もうひとつ、判断コストの話もある。
重要な処理やデータに限定してコメントを書く、という方針をよく聞く。しかし重要か否かを判断するのは意外に難しい。ならばいっそ「すべて書く」としたほうが、むしろ判断コストを低く抑えられると考えている。
ただし人には強制しない。チームの他のメンバーがコメントを書かないことは受け入れる。コメントは推奨であり義務感からではなく能動的に書いてほしい。そのため私は自分の担当部分にコメントのないコードが追加されたら代筆したり、GitHub の PR レビュー時に Suggestion して取り込んでもらったりする。
それでもすべては厳しい、という場合は public
や export
宣言で外部公開しているものに限定する手もある。
Q. 英語と日本語、どちらで書くべき?
開発メンバー間の共通言語を選ぶ、で OK。もっとも書きやすい言語を採用するのが望ましい。
日本語圏のメンバーしかいないのに英語コメント強制ルールを敷いて、失敗した例を知っている。英語圏のメンバーが参加するかも?とか意識の高さから決められたようだが、残念なことにコメントはほぼ書かれなくなっていた。英語を苦手とするメンバーが多かったのだろう。私もそうだった。
処理系の問題とか致命的な理由はなかったので、開発体制が変更される隙を狙って日本語コメントの許可を得た。その後は少しずつ、やがては全般的にコメントが書かれるようになった。結果、コードに関するコミュニケーション コスト削減など、目に見えて開発体験が向上した。
英語を否定しているわけでない。本気で英語を徹底したいなら、メンバーに学習機会を与えるか英語力で足切りすればよかった。このプロジェクトに関してそれは見合わなかったので、日本語コメントを許可して正解だったと思う。
Q. コードとコメントの整合性はどう担保する?
コメントが適切に書かれているなら整合性は自然に担保される。乖離が発生するならコメント対象が大幅に変更されたか、内容が不適切なので修正する機会かもしれない。
本記事の「文体」でコメントは目的、背景、注釈を意識すると書いた。
コード変更により目的や背景が変わることは極めて稀である。例えばある関数の処理を変えたとして、目的や背景に影響するだろうか?これらが変更されるレベルであれば関数も別名を検討したほうがよいし、ならばコメントも新たに書き起こす機会かもしれない。
コードの変更に影響されやすいのは注釈である。ただしこれも処理の説明をそのまま書く「悪いコメント例」でもない限り、滅多に乖離することはないだろう。
そういえば昔、コメントに騙された経験から否定派になった人と相談したことがある。それはまさに「悪いコメント例」だった。処理の説明だったので対象の変更に影響されて嘘になった。書かれるべきでないコメントだったとも言える。
これについてはコード レビューで防ぐか、見つけた時に修正するしかない。コードのバグと一緒である。あるコードにバグがあるからといってプロジェクト全体の実装を否定することはないだろう。コメントも同様で、もっというならレビューも含むチーム体制の問題である。
Q. 外部資料があればコメント不要では?
コメントはコードの側に書かれる仕様書、と認識しているので、コードに対する説明であれば必要と考える。
コードとの距離で考えるとコメントが最も近い。そのため API リファレンスやコードの注釈であれば、コメントにしたほうがよい。外部資料は大きな構造や設計方針の説明を目的としており、コメントと併存する。
コメントと直に競合しそうなものだと、Git のコミット ログや GitHub の PR だろうか。これらを詳細に書くならコメント不要という意見を聞いたこともある。
しかしこれらは距離が遠すぎる。コードについての説明ならば、コードとあわせて読みたい。そのうえで歴史的な経緯を知りたくなったら、改めてコミット ログや PR を探すのがよいと思う。
それと PR のレビューで説明を求められたら、コメントにも反映するとよいだろう。こうすれば以降の説明コストを省ける可能性がある。