POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

Zsolt Nagy

本記事は、原著者の許諾のもとに翻訳・掲載しております。

コード中にコメントを書くべきでしょうか? 是が非でも避けるべきでしょうか? それとも控えめに書けばいいでしょうか? 開発者たちはそれぞれ、ソフトウェアを開発する際にどのように、そしてどんな時にコメントを書くかについて、独自の考え方を持っています。この記事では私の意見を述べますが、これが誰にも当てはまるというわけではありません。

なお、関数型プログラミングまたはオブジェクト指向プログラミングの原則に則ってJavaScriptで書かれたソフトウェアに絞った上で、私の意見を述べることにします。

コメントと保守性

この記事では、保守性のあるコードを書く場合について考えます。つまり、以下のようなコードです。

  • 簡単に理解できる
  • 簡単に拡張できる
  • 簡単にデバッグできる
  • 簡単にテストできる

保守性のあるコードには、大量のコメントが必要でしょうか? 明確に書かれたコードであるならば、大量のコメントは不要だと私は考えています。コードを見れば内容が分かるはずだからです。

そもそも、保守性のあるコードにはコメントが必要なのでしょうか。いくつか例を挙げて、検証してみましょう。

コメントを極めるための演習

アプローチ1: くだらないコメントに埋もれたJavaScriptのコード

ByTheBook というニックネームを持つ若手の開発者が、以下のようなコードを書いたと想定します。

/**
 *  getWinner
 *
 *  参加者の配列から、最も高いスコアの勝者の名前を取得する。
 *  
 *
 *  @param {Array} participants (参加者)
 *  @return {string} name (名前)
 *  @throws {Error} wrong participant list (参加者のリストがおかしい)
 */
function getWinner( participants ) {

    var maxScore = 0;  // 最高スコアを保持するための一次変数
               // 常に参加者が見つけられるよう、最初は-1に
    var name = null;   // 最高スコアの参加者の名前
    // debugger; // デバッグ時にはuncomment
    // 参加者は配列なので、lengthプロパティがある
    for( var i = 0; i < participants.length; ++i ) {  // i: ループ変数
        var currentParticipant = participants[i]; // この参加者をチェック
        if( currentParticipant.score > maxScore ) {  // 最初はどんな数値でも score > null
            // 取得すべき新しい候補が見つかった場合
            name = currentParticipant.name;
            // !!! 最高スコアの更新を忘れないこと !!!
            maxScore = currentParticipant.score;
        }
    }

    // console.log( name, typeof name );

    // 返り値がstringであることを確認
    if( typeof name === 'string' ) {
        return name;
    } else { // 技術的にはこのelseは要らないけど
        throw new Error( 'Wrong participants' ); // 参加者リストがおかしい
    }
    // ここには到達しない
}

このアプローチの問題は何でしょう?

まず、コードの書き手が責任逃れをするかのようなコメントになっていることが問題ですね。アプリケーションを開発する際、コメントされたコードを残すようなことは決してしないでください。プロにあるまじき行為です。デバッグのために書いているのだとすれば、それはさらに悪いことです。きれいに消してください。手遅れだという場合は、コメント内容はローカルでコミットする習慣を身につけ、インタラクティブなrebaseによってコードをきれいにしていきましょう。

コメントされたコードを残したままにすることは大きな問題の中の取るに足らない一部分に過ぎません。技術的には、これらコメントのうち90パーセントは、単にノイズを加えているだけのようなものです。何の価値もなく、コードを読む人の気を散らすツールとしてのみ存在しています。例えば、あなたがこのコードを読んだ後、同点トップが2人以上いた場合に関数がどのように動くべきなのかといった問題にまで、気を配ることができましたか?

アプローチ2: 自己文書化コード

Flash は、ガッツのある若手開発者です。今度は彼がコメントを加えずに先ほどと同じコードを書いたと想定しましょう。

function getWinner( participants ) {
    if( participants.length === 0 ) return null;
    var lastIndex = participants.length - 1;
    var topParticipant = _.sortBy( participants, 'score' )[ lastIndex ];
    return topParticipant.name;
}

技術的に、コードは動いています。それに参加者がいなかった場合に null が戻り値となることも明らかです。しかし、同点トップが存在する場合にどういう動きをするのかについて、コードには何の説明もありません。さらに、関数の内容をすべて読まない限り、 participants オブジェクトの型が何かも分かりませんね。 participants はオブジェクトの配列で、要素に文字列型の name と整数型の score を含むというような情報を、すべてメソッドから読み解いていかなければならないのです。

アプローチ3: 行動を起こそうとしているだけのコード

Startupper は同点の際に起こる問題に気付きましたが、彼は他にやらなければならないことがたくさんあったので、コードに TODO コメント(やることリスト)を書いておきました。

// TODO: 最高スコアで並んだ2人の参加者が1位タイとなるときの場合分け
function getWinner( participants ) {
    if( participants.length === 0 ) return null;
    var lastIndex = participants.length - 1;
    var topParticipant = _.sortBy( participants, 'score' )[ lastIndex ];
    return topParticipant.name;
}

TODO のようなコメントの問題は、コード内で放っておかれがちだということです。やることリストの上に、さらにやることリストを書く場合も出てくるでしょう。やることリスト同士が矛盾しているかもしれませんし、コメントの説明が悪くて、元々何について書いていたのか誰も理解できないかもしれません。

上記の例においては、 TODO の内容が問題の本質を捉えていますが、解決策は全く与えられていません。

また、 participants 引数の型について、さらなる推測が必要だということにも、注意してください。

アプローチ4: 宣言的なヘッダコメント

Declarator は宣言的なヘッダコメントを書く習慣があり、彼のコメントには以下のような内容が含まれています。

  • 関数の動きについての短く的確な説明
  • 引数リストの型と戻り値
  • 例外
/**
 *  getWinner : 
 *    参加者リストのうち、最高スコアを取得した参加者の最初の1人の名前を返す。
 *
 *  @param {Array of Objects} participants (参加者)
 *            Elements: {string}  name (名前)
 *                      {integer} score (スコア)
 *  @return {Mixed(string|null)} name (名前)
 */
function getWinner( participants ) {
    if( participants.length === 0 ) return null;
    var lastIndex = participants.length - 1;
    var topParticipant = _.sortBy( participants, 'score' )[ lastIndex ];
    return topParticipant.name;
}

ここでは getWinner が何をするメソッドなのかが、明確に書かれています。コメントの大部分は役立つ内容ですし、コードを読む際も邪魔になりません。インラインでのコメントはなく、同点の場合についてもヘッダコメントで示されています。

アプローチ5: 症状ではなく、原因に働きかける

PickyPerfectionist は妥協しない開発者です。保守性の高いコードを書くことに誇りを持っており、「コードが十分に機能すればコメントは要らない」というマントラを実践しています。彼にはまた、十分に機能しないコードでは終わらせない、という信念があります。要するに、 PickyPerfectionist は決してコメントを書かないのです。

関数を記述する前に、 PickyPerfectionist はまず要件をきっちり確認します。同点の場合には全員の名前を出力し、コンマで切り分けます。

function getWinners( participants ) {
    var winnerNames = [];
    var winnerScore = null;

    for( var i = 0; i < participants.length; ++i ) {
        var currentName = participants[i].name;
        var currentScore = participants[i].score;
        if( currentScore > winnerScore ) {
            winnerScore = currentScore;
            winnerNames = [ currentName ];
        } else if ( currentScore === winnerScore ) {
            winnerNames.push( currentName );
        }
    }

    var result = winnerNames.join( ', ' );

    return result.length > 0 ? result : null;
}

コードは容易に理解できます。問題が根本的に解決されているので、同点になった場合どうするかなど、わざわざコメントを使って説明する必要がありません。また、ここでは participants オブジェクトの構造は定義されていませんが、 PickyPerfectionist はちゃんと先を読んでおり、コードから必要な情報をすべて入手する解決方法そのものを示しています。関数の名前さえも変更されています。問題となる点はどこにも無いようです。

ここでステークホルダーが仕様を変更し、複数の勝者を容認するのではなく1人だけに絞りたい、と要望してきたとしましょう。この変更によって、同点になった場合どうするか、という問題の原因そのものが無くなりました。その結果、決してコメントを書かないというこのアプローチは、既にアプローチ2で確認した次のコードに戻ってしまうことになります。

function getWinner( participants ) {
    if( participants.length === 0 ) return null;
    var lastIndex = participants.length - 1;
    var topParticipant = _.sortBy( participants, 'score' )[ lastIndex ];
    return topParticipant.name;

そのうちある開発者が、ちょっとおかしな事象に気が付きます。この関数は、最初に最高得点を獲得した参加者だけを返しているのです。そして、問題を認識した開発者が選択するアプローチによって、 TODO コメントを使う方法や、ステークホルダーから要求される仕様にすべて対応するコードを書く方法など、様々な解決法が提案されます。このように考えうるどの解決法にも、共通することが2つあります。

  • 時間がかかる
  • ある程度のノイズが加わる

あなたが開発者であるならば、コメントを使いますか? それとも、時間を無駄にしない別の方法をお考えですか? その場合でも、文書を確認することにも時間はかかる、ということを忘れないでください。

技術面から見ると、勝者は1人だけであることを確認し決定した上で、自動化されたテストが呼び出されます。ユニットテストにおけるこの振る舞いを保護することで、開発者は要求が正しくない変更に対応する必要がなくなります。しかしながらテストでは、問題を事前に指摘することはできません。特別な事情の場合に、万が一開発者が getwinner のメソッドを複数の勝者を扱えるように変更したとしても、既存のテストでは気が付くこともできないでしょう。

常識に則ったコメント

私としては、そのコメントが何らかの価値を加えるものであれば有益であると考えます。どのアプローチが絶対に正しいか、あるいは完全に間違っているか、というものではないと思っています。

コード内のコメントを禁止することは、カロリー計算を使ったダイエットとよく似ています。カロリー計算は、ある程度までは役に立つでしょう。1日に8000キロカロリーもしくは300キロカロリーの食事をするよりも、2000キロカロリーを食べるほうが間違いなく有効です。とはいえ、全体像を把握することは簡単なことではありません。他のものより有効なカロリーもあります。甘味料や炭酸入りのソフトドリンクを飲むよりも、サラダを食べる方が体に良いはずです。同じように、もしもコード内でのコメントが禁止されたならば、より良いコードを書かざるを得なくなるでしょう。しかし、コードにどうしてもコメントが必要な場合にもコメントを禁止してしまうと、長い目で見たときにかなりの害を及ぼすことになります。混乱を招き、保守性が低下することになりかねません。

では、どのような場面でコメントは有益なのでしょうか? 最初に考えることは、JavaScriptでは引数リストと戻り値の型が特定できない、ということです。そのため、型の定義に関する情報が必要となります。多くの他の言語では、こうした型の定義はメソッドのシグネチャに記載されています。JavaScriptの開発環境であれば、ホバー上でいくつかの型に関するデータが得られるかもしれません。概要をつかんでいれば時間を節約できるでしょう。 = == = = の違いを把握しており、その上で = = = を使いたいならば、型を理解しておくことが重要です。そして、これらの型を宣言することも同様に重要です。この宣言をしておかなければ、型の安全性が失われてしまいます。

シグネチャは、この例のようになっています。

/**
 *  listenTo
 *
 *  emitterがeventNameというイベントemitするとすぐにcallbackが呼ばれるように登録する。 
 *  
 *
 *  @param  {Object}    emitter
 *  @param  {string}    eventName
 *  @param  {function}  callback
 *
 *  @return {void}
 */
listenTo : function( emitter, eventName, callback ) { /* ... */ }

型ははっきりと特定されています。引数名が型の隣にちゃんと定義されています。3つの引数名はすべて、型の上の1文の中で記述されています。

戻り値の型を省略するべきかどうかについてははっきりしていません。そのため、コメントの記載事項を少なくするためにこれを省略しても、まったく問題はないでしょう。

クラスやメソッドを定義する段階では、ヘッダコメントを強くお勧めします。1つの文で1つのクラスとその責任範疇だけを記述します。メソッドについても同様です。ヘッダコメントは、プログラムを関数型のスタイルで書いている時にも有益です。

多くの開発者は、メソッドを実装する前にヘッダコメントを書いています。要求されている機能を、それ以上でも以下でもなく正確に実装するために、開発者は力を注いでいます。そこに焦点を当ててヘッダコメントを書けば、コードの質も向上します。

この他にも、ヘッダコメントについては以下の事を付記しておきます。

  • オプションの引数: 最後に出てくるいくつかの引数がundefinedのまま呼ばれることがあります。
  • mixed type: 型が混在して使われることがある。JavaScriptでは、同じ名前を使って複数の関数を定義することはできない。メソッドを多相化しオーバーロードするのではなく、同じメソッドの中であらゆる変種を実現する。すなわち、同じ関数が異なる型の引数を用いて使われることがある。
  • composite types: アプローチ4 の例。通常、 participants の型は array (配列)である。しかし構造を理解する目的のため、より多くのデータが提供される。
  • 例外処理に関する情報。
  • メソッドを呼び出すための前提条件。他のプログラマに、そのメソッドが機能すると推定される条件を伝える。
  • 副作用。

必要な情報だけを確定し、その他の部分は削除しましょう。簡潔であることが重要です。週末限定の短期的なプロジェクトであったり、もしくはプロトタイプ的なものであったりする場合と、開発してから5年間の保守を要するWebアプリの場合とでは、コメントに求められる要件も異なります。

優れたヘッダコメントを書くということは、ひとつの技術です。とはいえ、常識に従えば十分に読みやすいヘッダコメントが書けます。常識を使うだけで、きちんと情報が網羅されたヘッダコメントになります。次の例は悪いヘッダコメントの見本です。

/**
 *  onRender
 *
 *  Callback on render
 *
 *  @return {void}
 */
onRender : function() {
    this.removeAllBindings();
    this.cleanOpenedWidgets();
}

こちらは、もう少しましな例です。

/**
 *  onRender: コンポーネントのレンダリング前に呼ばれる。
 *  model-viewの結合をクリーンアップし、開かれたウィジェットのノードを
 *  削除する。
 *
 *  @return {void}
 */
onRender : function() {
    this.removeAllBindings();
    this.cleanOpenedWidgets();
}

きちんと書かれたヘッダコメントがあれば、コード内のコメントの99.99%は不要になります。メソッドの複雑さがどうであれ、コード内のコメントはただノイズを付加するだけであり、開発者を助けるというよりむしろ気を散らせる、という意見に私はほぼ同意します。

ヘッダコメントを使えば、コードは単体ごとに機能を持つ、という原則にも従うことができます。コードに求めるレベルがどの程度であれ、1つの構成ブロックはそれぞれ1つずつ、しっかり定義された機能を持たなければなりません。そのためには、関数に対するコメントと同じように、クラスにもヘッダコメントを加えることをお勧めします。クラスの定義付けに苦労している場合、それは単体ごとに機能を持つという原則に違反している可能性を示しています。

ヘッダコメントを書くことが難しいと感じる状況とはすなわち、1つのメソッドを複数に再構成したり、オブジェクトの責任をどこかに移行したりすることを検討するべき状況であることを意味します。つまり、ヘッダコメントについて検討すれば、より良いコードへと繋がっていくのです。

コードの保守という観点から見た場合、ヘッダコメントには少なくとも次の2つの目的があり、有益であると言えます。

  1. コードのレビュアーが、メソッドの目的について簡単に概要を把握したいだけの場合、ヘッダコメントを使えば時間を節約できる。
  2. 誤ったメソッドが確認された場合、レビュアーはヘッダコメントを見れば、実装のベースとなった元々の目的を確認できる。

こうした情報を伝えるためにも、ヘッダコメントは随時メンテナンスする必要があります。自分が何を行っているか把握している開発者にとっては、それはそんなに難しい作業ではないでしょう。

結局コメントは必要か?

コメントを書くか書かないかは、結局開発者次第です。役に立つと思う方法を実践してください。中には、コメントを一切必要としないプロジェクトもあるでしょう。厳密な型分けを必要とする言語とは違い、JavaScriptではコメントを使って、引数や戻り値に関する重要な情報を示すことができます。コンパクトなヘッダコメントを使えば、コードがより分かりやすくなります。コンパクトなヘッダコメントを作成することによって更に、コード自体に対する検討が進み、より保守性の高いコードへと発展させることもできるでしょう。

今まで書いてきたように私個人では、コメントはすべて役に立たないものとは考えていません。完全に黒、もしくは完全に白、という世界なんてありません。極端な意見は多くの論議を惹きつけます。しかし長い目でみれば、双方の観点から検証されているバランスのとれたアプローチの方が、より有益であるものなのです。