Chienomi

コードに入れるコメントの書き方

プログラミング::beginners

コードのコメントの入れ方については色々主張があるようで、かなり攻撃的にこれは悪いコメントだ、こう書くべきだという主張を見かける。

だが、コードに書くべきコメントは何が適切かというのは状況によって大きく異なるた、一概にどう書くべきであるということはできない。

この記事ではコメントを入れるにあたって一貫性のあるルールとしてどのようなことが考えられ、 実際にどのように書くかということについて検討する。

なお、当然ながら私も完全無欠のコメントを書けるわけではないので、ご承知おきいただきたい。

実例

ベースのコード

Chienomiで使っているLightboxスクリプトから。

(function() {
  if (! document.addEventListener ) { return false; }
  var $e = function(id) { return document.getElementById(id) }
  Element.prototype._tn = Element.prototype.getElementsByTagName
  
  var wrapper = $e("WrapWindow")
  var fadingTimer = false
  var alpha = 0.0
  var lboxImage = $e("LBoxImage")
  
  var fadeout = function() {
    if (alpha < 0.8) {
      alpha = alpha + 0.05
      wrapper.style.backgroundColor = "rgba(0,0,0," + alpha + ")"
    } else {
      clearInterval(fadingTimer)
      fadingTimer = false
    }
  }
  
  var setLightboxTrigger = function(e) {
  
    lboxImage.src = e.currentTarget.src.replace("/thumb/", "/full/")
    lboxImage.style.maxHeight = window.innerHeight || document.documentElement.clientHeight
    lboxImage.style.maxWidth = window.innerWidth || document.documentElement.clientWidth
  
    wrapper.style.visibility = "visible"
    fadingTimer = setInterval(fadeout, 30)
  }

  wrapper.addEventListener("click", function(e) {
    if (fadingTimer) {
      clearInterval(fadingTimer)
    }
    wrapper.style.backgroundColor = "transparent"
    wrapper.style.visibility = "hidden"
    alpha = 0.0
    lboxImage.src = ""
  }, false)

  
  var art = $e("MainArticle")
  var figs = art._tn("figure")
  for(var i=0,l=figs.length; i<l; i++) {
    var fi = figs[i]._tn("img").item(0)
    if ( fi.src.indexOf("/thumb/") >= 0 ) {
      fi.addEventListener("click", setLightboxTrigger, false)
    }
  }
})()

初心者向けにコメントを入れる

// このスクリプトはDOMコンテンツがロードされた状態でロードされる
// つまり、ドキュメント内のHTML要素は全てロード済みであり、取得・操作することができる

// 括弧で囲まれた匿名関数
// 括弧で囲まれた匿名関数を使い、名前空間を汚さないようにする
// (function() { ... } )() で呼び出せる
(function() {
  // 機能チェック。document.addEventListenerが定義されていなければ終了
  if (! document.addEventListener ) { return false; }
  // document.getElementByIdのショートハンド
  var $e = function(id) { return document.getElementById(id) }
  // document.getElementsByTagNameのショートハンド。必要性は乏しい
  // プロトタイプ内に同プロトタイプで定義されているプロパティの別名を作る
  Element.prototype._tn = Element.prototype.getElementsByTagName
  
  var wrapper = $e("WrapWindow")   /* idがWrapWindowの要素 (Node) */
  var fadingTimer = false          /* 変数宣言。タイマーで使う */
  var alpha = 0.0                  /* 透過度として使う値。0.0で透明、1.0で不透明 */
  var lboxImage = $e("LBoxImage")  /* #WrapWindow に内包された idがLBoxImageの要素 (Node) */
  
  // 匿名関数をfadeoutに代入
  // function fadeoutと書いても同じ
  var fadeout = function() {
    // 0.8までalphaの増加を続ける
    if (alpha < 0.8) {
      alpha = alpha + 0.05
      wrapper.style.backgroundColor = "rgba(0,0,0," + alpha + ")"
    } else {
      // alphaが0.8に達したので繰り返し呼び出されるイベントをリセット、タイマーフラグをリセット
      clearInterval(fadingTimer)
      fadingTimer = false
    }
  }
  
  // イベントコールバック関数
  // 匿名関数をsetLightboxTriggerに代入する。
  // イベントコールバック関数の引数としてEventオブジェクトをeとして受け取る
  var setLightboxTrigger = function(e) {
    // lboxImageはimg要素であり、srcは画像のURL
    // e.currentTargetはイベントを発生させた要素で、これはサムネイル画像のimg要素
    // e.currentTarget.srcでサムネイル画像のURLで、このうち/thumb/を/full/に置き換える
    // ** /thumb/ の/はパスセパレータでありregexリテラルではない **
    lboxImage.src = e.currentTarget.src.replace("/thumb/", "/full/")
    // 画像の最大の高さ、幅はビューポートいっぱい
    // maxHeight, maxWidthの指定なので、ビューポートよりも小さい画像を伸長するわけではない
    lboxImage.style.maxHeight = window.innerHeight || document.documentElement.clientHeight
    lboxImage.style.maxWidth = window.innerWidth || document.documentElement.clientWidth
  
    // CSSを visibility: visible にセットして見えるようにする
    wrapper.style.visibility = "visible"
    // フェードインするためのタイマーをセット。
    // fadingTimerは外側のスコープで定義されているので、タイマーを捨てるときや止めるときに参照できる
    fadingTimer = setInterval(fadeout, 30)
  }

  // wrapperのクリックイベント。
  // つまりは、wrapperが表示されている = 画像表示状態でクリックされたときのイベント
  // これはタップでも起動される
  wrapper.addEventListener("click", function(e) {
    // fadingTimerがセットされているならクリアしておく
    // これは、「表示途中でキャンセルする」動作
    if (fadingTimer) {
      // clearIntervalはsetIntervalでセットしたタイマーを停止する
      clearInterval(fadingTimer)
    }
    // wrapperを透明にする
    wrapper.style.backgroundColor = "transparent"
    // wrapperの表示を隠す。この状態でも見えないだけでエレメントは存在する
    wrapper.style.visibility = "hidden"
    // 透明度を透明に
    alpha = 0.0
    // 画像は消す
    lboxImage.src = ""
  }, false)

  // idがMainArticleである要素
  // これは article#MainArticle で、lightboxの対象になるのはこれに内包されているもののみであるため
  var art = $e("MainArticle")
  // 「idがMainArticleである要素に内包されているfigure要素
  var figs = art._tn("figure")
  // …に対するイテレータ
  for(var i=0,l=figs.length; i<l; i++) {
    // 各該当するfigure要素内のimg要素に対してイベントリスナを設定する
    // 仕様上、figureに内包されているimg要素はひとつだけ。
    // それを取得する方法は色々あるものの、img要素のコレクションを取得し、最初のアイテムを取得する
    var fi = figs[i]._tn("img").item(0)
    // srcに/thumb/が含まれる場合のみが対象。
    // 逆に言えば、/thumb/を含まない場合はスキップする
    if ( fi.src.indexOf("/thumb/") >= 0 ) {
      // 該当したimg要素にイベントリスナを設定する
      // 中身は先程定義したsetLightboxTrigger
      fi.addEventListener("click", setLightboxTrigger, false)
    }
  }
})()

初心者にとってはコードの見た目からコードのフローを追いかけたり、自明である箇所を抽出することができない。 そのため、初心者向けのコメントというのは 自明であることをなぜ自明であるかを述べるものであるべきである

つまり、Whatを書く。

一般的にはこのようなコメントがなされたコードは読みづらい。 コメント量が多すぎてコードが埋もれて、可読性が下がるからだ。

だが、初心者の場合はまだ覚えていることが少ないので、何度も同じ情報を反復してたどることになる。 字面が何を意味しているかも説明することで学習の助けになる。

初級者向けにコメントを入れる

(function() {
  // 機能チェック。document.addEventListenerが定義されていなければ終了
  if (! document.addEventListener ) { return false; }
  var $e = function(id) { return document.getElementById(id) } /* ショートハンド */
  Element.prototype._tn = Element.prototype.getElementsByTagName /* ショートハンド */
  
  var wrapper = $e("WrapWindow")   /* idがWrapWindowの要素 (Node) */
  var fadingTimer = false          /* 変数宣言。タイマーで使う */
  var alpha = 0.0                  /* 透過度として使う値。0.0で透明、1.0で不透明 */
  var lboxImage = $e("LBoxImage")  /* #WrapWindow に内包された idがLBoxImageの要素 (Node) */
  
  var fadeout = function() {
    // 0.8までは不透明化を続けるため、alphaを増加させるためにタイマーによって繰り返し起動される
    if (alpha < 0.8) {
      alpha = alpha + 0.05
      wrapper.style.backgroundColor = "rgba(0,0,0," + alpha + ")" /*新しいalphaのセット */
    } else {
      // alphaが0.8に達したので繰り返し呼び出されるイベントをリセット、タイマーフラグをリセット
      clearInterval(fadingTimer)
      fadingTimer = false
    }
  }

  // 画像クリックイベント用のコールバック関数
  var setLightboxTrigger = function(e) {
    // lboxImageはimg要素であり、srcは画像のURL
    // e.currentTargetはイベントを発生させた要素で、これはサムネイル画像のimg要素
    // e.currentTarget.srcでサムネイル画像のURLで、このうち/thumb/を/full/に置き換える
    // ** /thumb/ の/はパスセパレータでありregexリテラルではない **
    // これにより、/thumbを/fullに置き換えたパスにフルサイズの画像を置けばフルサイズの画像がロードできる
    lboxImage.src = e.currentTarget.src.replace("/thumb/", "/full/")
    // 画像の最大の高さ、幅はビューポートいっぱい
    // maxHeight, maxWidthの指定なので、ビューポートよりも小さい画像を伸長するわけではない
    lboxImage.style.maxHeight = window.innerHeight || document.documentElement.clientHeight
    lboxImage.style.maxWidth = window.innerWidth || document.documentElement.clientWidth
  
    wrapper.style.visibility = "visible"
    // fadingTimerは外側のスコープで定義されているので、タイマーを捨てるときや止めるときに参照できる
    fadingTimer = setInterval(fadeout, 30)
  }

  // wrapperが表示されている = 画像表示状態でクリックされたときのイベント
  wrapper.addEventListener("click", function(e) {
    // fadingTimerがセットされているならクリアしておく
    // これは、「表示途中でキャンセルする」動作
    if (fadingTimer) {
      // clearIntervalはsetIntervalでセットしたタイマーを停止する
      clearInterval(fadingTimer)
    }
    
    // Lightboxウィンドウの消去処理。実際は見えなくしているだけ
    wrapper.style.backgroundColor = "transparent"
    wrapper.style.visibility = "hidden"
    alpha = 0.0
    lboxImage.src = ""
  }, false)

  // idがMainArticleである要素
  // これは article#MainArticle で、lightboxの対象になるのはこれに内包されているもののみであるため
  var art = $e("MainArticle")
  var figs = art._tn("figure")
  for(var i=0,l=figs.length; i<l; i++) {
    // 各該当するfigure要素内のimg要素に対してイベントリスナを設定する
    // 仕様上、figureに内包されているimg要素はひとつだけ。
    // それを取得する方法は色々あるものの、img要素のコレクションを取得し、最初のアイテムを取得する
    var fi = figs[i]._tn("img").item(0)
    // 対象エレメントは置き換え元になる /thumb ディレクトリをパスに内包しているもののみ
    if ( fi.src.indexOf("/thumb/") >= 0 ) {
      fi.addEventListener("click", setLightboxTrigger, false)
    }
  }
})()

初級者は自明であることを理解できるが、なんのためにそうするのかということがわからない。 そこで、そのコードの意味するところを書く。つまりは、Whyを書く。

初心者向けのコメントではこのプログラミング言語が持っている機能に関する説明もあったが、初級者であればさすがにその必要はあるまい。 しかし、応用力の問題から実際に適用されたコード上の表現に関しては難しいものは説明があったほうが良い。

中級者向けにコメントを入れる

(function() {
  if (! document.addEventListener ) { return false; } /* Element.addEventListenerを持たない環境で機能させる必要はない */

  /*** Define shorthands ***/
  var $e = function(id) { return document.getElementById(id) }
  Element.prototype._tn = Element.prototype.getElementsByTagName
  
  var wrapper = $e("WrapWindow")   // 画面全体を覆うモーダルウィンドウ
  var fadingTimer = false          // setIntervalのタイマー用
  var alpha = 0.0                  // モーダルウィンドウのalpha
  var lboxImage = $e("LBoxImage")  // #WrapWindow に内包された img要素
  
  // 表示の際のfade-in関数 (感覚上、下側のArticleが消えていくように見えるためfadeoutとする)
  var fadeout = function() {
    // set alpha upto 0.8
    if (alpha < 0.8) {
      alpha = alpha + 0.05
      wrapper.style.backgroundColor = "rgba(0,0,0," + alpha + ")"
    } else {
      clearInterval(fadingTimer)
      fadingTimer = false
    }
  }

  // 画像クリックイベント用のコールバック関数
  var setLightboxTrigger = function(e) {
    // サムネイル画像のパス /thumb ディレクトリを /full ディレクトリに置き換えることでフルサイズ画像が取得できる
    // ** /thumb/ の/はパスセパレータでありregexリテラルではない **
    lboxImage.src = e.currentTarget.src.replace("/thumb/", "/full/")
    lboxImage.style.maxHeight = window.innerHeight || document.documentElement.clientHeight
    lboxImage.style.maxWidth = window.innerWidth || document.documentElement.clientWidth
  
    // 表示
    wrapper.style.visibility = "visible"
    fadingTimer = setInterval(fadeout, 30)
  }

  // wrapperのクリックイベント == モーダルウィンドウ消去イベント
  wrapper.addEventListener("click", function(e) {
    // 表示途中ならばキャンセルする
    if (fadingTimer) {
      clearInterval(fadingTimer)
    }
    
    wrapper.style.backgroundColor = "transparent"
    wrapper.style.visibility = "hidden"
    alpha = 0.0
    lboxImage.src = ""
  }, false)

  // article#MainArticleに内包されているもののみ対象
  var art = $e("MainArticle")
  var figs = art._tn("figure")
  for(var i=0,l=figs.length; i<l; i++) {
    // 仕様上、figureに内包されているimg要素はひとつだけ。
    // 対象エレメントは置き換え元になる /thumb ディレクトリをパスに内包しているもののみ
    var fi = figs[i]._tn("img").item(0)
    if ( fi.src.indexOf("/thumb/") >= 0 ) {
      fi.addEventListener("click", setLightboxTrigger, false)
    }
  }
})()

中級者は言語として知りうる情報をわざわざ説明する必要はない。 しかしコード上明らかでないものもあり、コメントによって情報を付与する必要がある。

例えば、早期に定義されるがなかなか使われない変数、何のために分けられているのか不明な関数、 暫定的な仕様、このロジックが成立する前提となる理由、このようなコードになった意図や経緯などだ。

中級者向けコメントに必要なのは、コードそのものを理解することではなく、コードを時間的に把握することである。 つまり、「現在の状態でなくなっても意味が把握しやすく、変更を加えても破綻しないように」書くのである。 だから、コメントはinspectionを意識したものになる。 コードに変更を加えるときにコードを部分的に読む時間を短縮するためのコメントだ。

Guru向けにコメントを入れる

(function() {
  if (! document.addEventListener ) { return false; } 

  /*** Define shorthands ***/
  var $e = function(id) { return document.getElementById(id) }
  Element.prototype._tn = Element.prototype.getElementsByTagName
  
  var wrapper = $e("WrapWindow")
  var fadingTimer = false
  var alpha = 0.0
  var lboxImage = $e("LBoxImage")
  
  // showing animation
  var fadeout = function() {
    if (alpha < 0.8) {
      alpha = alpha + 0.05
      wrapper.style.backgroundColor = "rgba(0,0,0," + alpha + ")"
    } else {
      clearInterval(fadingTimer)
      fadingTimer = false
    }
  }

  // showing callback
  var setLightboxTrigger = function(e) {
    // path/to/(thumb|full)/image.ext
    lboxImage.src = e.currentTarget.src.replace("/thumb/", "/full/")
    lboxImage.style.maxHeight = window.innerHeight || document.documentElement.clientHeight
    lboxImage.style.maxWidth = window.innerWidth || document.documentElement.clientWidth
  
    wrapper.style.visibility = "visible"
    fadingTimer = setInterval(fadeout, 30)
  }

  // Remove lightbox (cancel)
  wrapper.addEventListener("click", function(e) {
    if (fadingTimer) {
      clearInterval(fadingTimer)
    }
    
    wrapper.style.backgroundColor = "transparent"
    wrapper.style.visibility = "hidden"
    alpha = 0.0
    lboxImage.src = ""
  }, false)

  // Select and set event.
  var art = $e("MainArticle")
  var figs = art._tn("figure")
  for(var i=0,l=figs.length; i<l; i++) {
    var fi = figs[i]._tn("img").item(0)
    if ( fi.src.indexOf("/thumb/") >= 0 ) {
      fi.addEventListener("click", setLightboxTrigger, false)
    }
  }
})()

Guruにくどくどとした説明など必要ない。 意味が明瞭なコードさえかけばGuruは瞬く間に全てを把握してしまうだろう。

だからコメントは検索用のタグであり、またコードセクションが何にまつよるものかを示すためのマーカーである。

実際のコメント

(function() {
  if (! document.addEventListener ) { return false; }
  var $e = function(id) { return document.getElementById(id) }
  Element.prototype._tn = Element.prototype.getElementsByTagName
  
  var wrapper = $e("WrapWindow") /* ModalWindow */
  var fadingTimer = false /* IntervalTimer */
  var alpha = 0.0 /* ModalWindows's alpha number */
  var lboxImage = $e("LBoxImage") /* target img object */
  
  /* fading out (interval callback) */
  var fadeout = function() {
    if (alpha < 0.8) {
      alpha = alpha + 0.05
      wrapper.style.backgroundColor = "rgba(0,0,0," + alpha + ")"
    } else {
      clearInterval(fadingTimer)
      fadingTimer = false
    }
  }
  
  /* set this for event callback */
  var setLightboxTrigger = function(e) {
  
    /* Set next image */
    lboxImage.src = e.currentTarget.src.replace("/thumb/", "/full/")
    lboxImage.style.maxHeight = window.innerHeight || document.documentElement.clientHeight
    lboxImage.style.maxWidth = window.innerWidth || document.documentElement.clientWidth
  
    wrapper.style.visibility = "visible"
    fadingTimer = setInterval(fadeout, 30)
  }

  /* Return from lightbox */
  wrapper.addEventListener("click", function(e) {
    if (fadingTimer) {
      clearInterval(fadingTimer)
    }
    wrapper.style.backgroundColor = "transparent"
    wrapper.style.visibility = "hidden"
    alpha = 0.0
    lboxImage.src = ""
  }, false)

  
  /***** Set event listener *****/
  
  var art = $e("MainArticle")
  var figs = art._tn("figure")
  for(var i=0,l=figs.length; i<l; i++) {
    var fi = figs[i]._tn("img").item(0)
    if ( fi.src.indexOf("/thumb/") >= 0 ) {
      fi.addEventListener("click", setLightboxTrigger, false)
    }
  }
  
})()

このコードは他の人が読むことはないため、改修するときに目的の箇所を素早く見つけられるようにする目的である。

これは、エディタの色分け機能によってコメントのみを眺めた場合に見るべき場所を見つけられる、というのがポイントだ (もちろん、コメントが比較的派手なカラーテーマを使うのである)。

また、個人的には変数宣言は何のための変数かというのを見失いやすいので、明瞭でない場合はコメントを入れるようにしている。

コメント以前にすべきこと

コメントが重要視され、コメントで技量を測るような発言がSNSなどでは散見されるが、 実のところコメントというのはあくまでも可読性を高めるための補助的な存在であり、決してコメント こそが 大事なわけではない。

コメント以前に、わかりやすい変数名をつけ、一時変数と区別がつきやすいようにし、適切なインデントと改行を行い、明瞭なロジックによって「そもそも読みやすいコードを書く」ということを心がけるほうが先であり、例示した程度の規模であれば、読みやすく書きさえすればコメントは必要ないとすら言える。

コメントについてばかり声高に叫ぶのは稚拙だ。

「読みやすいコード」というと、オライリー刊の “リーダブルコード” を引き合いに出す人が多いが、それを盲信するのは非常に危険だ。 「読みやすい、一見にして明瞭なコードとはどんなものか」ということについて常に意識を持ち、日々考えることは不可欠なのだ。

故に、「コメントの入れ方」という観点から考えるようなものではない。 コードを読みやすく書く、と考えたときに、読みやすいコードを実現するパーツとして、自然と明瞭なコメントを入れることになるだろう。 そして、「読みやすいコード」を考えるとき、「誰にとって読みやすいか」ということを考えずにそれを指向することはできないのだ。