ハクソク

世界を動かす技術を、日本語で。

コメントは「何」を説明すべきかもしれない (2017)

概要

  • コメントは「なぜ」だけでなく「何を」も説明すべきという主張
  • クリーンコードとコメントの役割の違いを解説
  • コンテキストスイッチの弊害を指摘
  • メソッド分割とコメントのバランスを議論
  • コメントの「何を」説明する有用性を具体例で提示

コメントは「なぜ」だけでなく「何を」も説明すべき理由

  • 一般的に「コメントはなぜを書くべき、何を書くべきではない」とされる風潮
  • しかし、コードだけで全てを明確にするのは困難な場合も存在
  • 例:
    • //weight, radius, price
      w = 10, r = 9, p = 1
      よりも
      weight = 10, radius = 9, price = 3
      の方が明確
  • 省略名(w, r, p)は直後なら意味が分かるが、後で見たときに混乱しやすい
    • 例: 「w」を「width」と誤解するリスク
  • コメントはクリーンコードの代用にならないが、補助的な役割として有効

「なぜ」をコメントで説明する重要性

  • 「なぜ」はコミットメッセージやテストに残すべきという意見もある
  • 例:
    // Clear twice to deal with bug ABC in library XYZ, see [link]
    XYZ.clear();
    XYZ.clear();
    • このコメントがなければ、「なぜ2回呼ぶのか」理解できず誤った修正をする恐れ
  • コミット履歴を遡るのは手間であり、コンテキストスイッチが発生
  • コメントで直接理由を記載することで即座に意図を理解できる

説明的なコードが逆に理解を妨げる場合

  • Bob Martinの「Extract Till You Drop」例

  • 説明的なメソッド分割によりコードが短く見えるが、全体理解には複数メソッドの追跡が必要

  • バグ修正時など、何度もメソッド間を移動する手間が発生

  • コメントで「何を」説明することで一箇所で理解できる利点

    • 例:
      Pattern symbolPattern = Pattern.compile("\$([a-zA-Z]\w*)"); // 例: $F1a3
      Matcher symbolMatcher = symbolPattern.matcher(stringToReplace); // 全シンボルを置換
      while (symbolMatcher.find()) { ... } // ここで全インスタンスを置換

「何を」説明するコメントの有用性

  • 全てのケースでコメントが必要なわけではない
  • しかし、一部のケースでは「何を」説明するコメントが最適解
  • コメントを全否定せず、状況に応じて柔軟に活用する姿勢の重要性

Uncle Bob批判と賛同するエッセイ紹介

  • Uncle Bobの例をよく批判しているが、「The Lurn」というエッセイには賛同
  • プログラミング言語学習の過剰評価よりも、他の有用な知識の重視を提案

  • 結論: コメントは「なぜ」だけでなく「何を」も適切に説明することで、コードの可読性と保守性を向上させる
  • クリーンコードコメントのバランスを意識した開発姿勢が重要

Hackerたちの意見

もう真面目にアンクルボブスタイルのプログラミングを使ってる人はいない気がするな(各行をそれぞれのメソッドに分けるやつ)。一時期は流行ってたけど、あんなコードベースのバグを直そうとしたことがある人なら、この記事が言ってることがよくわかるはず。定義に飛ぶキーを何度も押すのが本当にイライラするし、順番に動く別々の部分を行ったり来たりするのも面倒くさい。あの本があんなに大きくなった理由が全くわからない。試してみれば、めちゃくちゃ面倒で可読性には全然役立たないってわかるのに。
本を書いて「クリーンコード」ってタイトルで出版するのは、めちゃくちゃマーケティングになるんだよね。あのスタイルについて、実はそんなにシンプルじゃないって議論をたくさんしたけど、相手はただ本を指差すだけだった。
> もう真剣に叔父ボブスタイルのプログラミングを使ってる人はいない気がする(各行が自分のメソッドに抽出されるやつ)でも、そういうのを楽しむGoの人たちがたくさんいるんだよね(8ファイルを通してインターフェースがインターフェースを呼び出すのを見てた頃を思い出す…結局「この暗号キーを設定する」ってだけになったし、最初からトップにあったらよかったのに)。
IDEの「レンズ」拡張機能を作るのはどれくらい大変なんだろう?マウスオーバーしている関数の再帰的にインライン化されたバージョンを、自動的に表示してくれるやつ。例えば、20行未満の時とか。
IDEの拡張機能で、マウスオーバーしている関数の再帰的にインライン化されたバージョンを自動的に表示してくれるのは、どれくらい難しいんだろう?例えば、20行未満の時とか。
彼が本物のプログラマーに出会った時の例はこれを見てみて: https://github.com/johnousterhout/aposd-vs-clean-code _A Philosophy of Software Design_ はすごく素晴らしくて、過小評価されている本だよ: https://www.goodreads.com/en/book/show/39996759-a-philosophy... 俺はこれを超おすすめするし、俺のコードを明らかに改善してくれた --- もう一冊の本は、上司のデスクに置かれた時に彼の能力を疑わせるもので、でもその後モニターの下に置かれて、彼の意見を反映してたな…。
書かれた時代や対象が違ったからね。(t, p, luみたいな変数名が普通だった頃)自分には役立ったし、他の人にもそうだったと思うけど、あんまりそのアドバイスを文字通りに受け取ったことはないな(作者がそう意図してたとしても)。他の本や議論、アドバイス、経験を基に、私は「長い(例えば、複数ページの)関数は悪い」と覚えておくことにしてる。今のコンピュータサイエンスの卒業生はもっと良く知ってると思うけど、これは業界の常識になったからね。
それ、めっちゃ共感するわ。「各クラスはそれぞれのファイルに」とか「各関数はそれぞれのファイルに」とか言われるスタイルには本当にイライラする。必要なものは目の前に全部あった方がいいのに、無理にバラバラにする必要なんてないよね。前に働いてた会社でこれを言ったら、「すごく整理されてる」とか言われて笑われたわ。結局、普通の人は批判的に考える力がゼロだってことだね。
> 各行がそれぞれのメソッドに抽出される ジョン・カーマックが言ったように、「多くの操作が順番に行われるべきなら、そのコードも順番に書くべきだ」(https://cbarrete.com/carmack.html)。数行の単一メソッドは読みやすいけど、メソッド間を行ったり来たりするのは気が散るし遅い。プロセッサが様々なRAMの場所を読み込むのと同じようにね。言語によっては、多くの行を書く理由がある場合もある。例えば、Javaではメソッドが複数のプリミティブ値を返せないから、パフォーマンスのためにプリミティブにこだわるなら、インラインにして中括弧で内部のスコープを制限する必要がある。
その頃のRubyエコシステムは「DRY」(対 WET)や間接的な書き方が特にひどかった。Sandi Metzが「実践的オブジェクト指向デザイン」を紹介するまでは、かなり厳しかったよ。これがきっかけで「賢い」とか「職人技」とか「エレガント」から、未来のプログラマーにとってもっと実用的な方向に動き出したと思う。Rubyコードのデバッグをしたとき、スタックトレースの行が存在しないことを覚えてる人いる?コードが実行時に動的に生成されてボイラープレートを減らすためにね。ペパリッジファームは覚えてるよ。
ボブ・マーチンの例を鵜呑みにするのはやめた方がいいよ。「アンクルボブみたいにリファクタリングしないで」っていうのもあるしね: https://theaxolot.wordpress.com/2024/05/08/dont-refactor-lik...
ありがとう!まだ若手だった頃にそのリファクタリングのアドバイスをそのまま受け入れたせいで、すごく過剰に抽象化されたフレームワークを書いちゃって、デバッグや新機能追加のときに何年も苦しむことになった。習慣を変えるのは簡単じゃなかったよ…
この例は、機能の目的を説明するトップコメントブロックがあれば、もっと良くなると思う。良い使用例も示してね。こんな感じで、理想的には実行可能なドキュメントコメントを使って、コメントがコードを正しく説明するのを助けるといい。入力文字列のシンボルプレースホルダーを翻訳された値に置き換えて、"$foo"形式のシンボルプレースホルダーをスキャンする。認識されたシンボルはそれぞれ対応する値に置き換えられる。シンボルは、シンボルが存在する場合(つまり、getSymbol(String)がnullでない場合)にのみ置き換えられるし、今回の呼び出しで既に置き換えられていない場合に限る。例: - 入力 = "Hello $name, welcome to $city!" - 出力 -> "Hello Alice, welcome to Boston!" シンボルプレースホルダーが置き換えられた文字列を返す。
こういうトップコメントに反対する意見は、コードを変更したときにそれに関連するコメントを更新するのを忘れやすくなるってことだよね。1行のコメントは、パッチを当てるときに変更が必要だってすぐにわかるから、読みやすくていいよね。
「何をするか」と「なぜするか」の区別は、コードが読者が文脈から推測できないドメイン知識をエンコードしているときに崩れる。俺は会計ソフトを作ってるけど、半分の「何をするか」のコメントは、そうでなければ理解できないビジネスルールを説明してる。例えば、こんな感じで: // 銀行振込は2つのトランザクションとして現れる - ここでのデビットと他所でのクレジット // 3日間のウィンドウ内で等しい逆の金額を探してマッチさせる これが「何をするか」を説明してるけど、同時に暗に「なぜ」も説明してる - それがダブルエントリーの仕組みで、銀行が決済遅延に許容する範囲だから。これをメソッド名に抽出するのは無理がある。extractNameMatchingTransfersWithinSettlementWindow()なんて名前じゃ全然助けにならないし、結局、決済ウィンドウが何かを理解しなきゃいけなくなるし、3日間の文脈も失っちゃう。
俺もそう思うけど、ビジネスルールやクセは「なぜ」にカウントされると思うよ。
メソッドの呼び出し元はその3日を知っておく必要があるの?なんで?もしFED Nowにアップデートしたら変わるかもしれないし。呼び出し元は、ウィンドウ内をチェックしてるってことだけ知ってればいいんじゃないかな。それはおそらく定数(SETTLEMENT_WINDOW_DAYSみたいな)で、文脈を提供するものだし。必要なら、その定数に3日がなぜ必要かを説明するコメントを追加すればいいと思う。
正規表現には「何をするか」のコメントをいつも使ってるよ。それに、前後の変換の例も提供して、将来の開発者がすぐに何が起こってるか理解できるようにしてる。最初に立ち止まって、文脈を切り替えて、ヒエログリフを解読する必要がないからね。
> それは「何をするか」を説明してるけど、同時に「なぜ」も暗に説明してるんだよね。ダブルエントリーがこうやって機能するから、銀行が決済遅延に許容する範囲なんだ。これをメソッド名に抽出するのは無理があるよね。だから、可能な限り定数を明示的に分解するようにしてる。例えば `ageAlertThresholdHours = backupIntervalHours + approxBackupDurationHours + wiggleRoomHours` みたいに。確かに、これには4つの定数が必要で、「28時間」よりもタイプするのが長くなるけど、これが私の考え方を伝えてるし、この28時間がどうやって決まってるか、アラートがうるさいときにどう調整するかもわかる。もし誤検知が出たら、29時間に増やすのが簡単だってこともわかるし。こうやって、バックアップが例えば3時間かかるはずなのに、今は4時間かかってるってことも見える。だから、監視データに基づいておおよそのバックアップ時間を増やす「明らかに正しい」修正ができるんだ。
ビジネスの「何をするか」の要約や詳細が、「なぜそのコードが存在するのか/こう書かれているのか」の最も重要な部分だと思う。これはそのコードのセクションが解決しようとしている全体的な問題なんだよね!それに、新しいシニアメンバーが勝手にリファクタリングするのに対する良い防御にもなる。ビジネスルールには反論できないけど、叔父ボブには反論できるからね。 ;)
あなたが説明しているのは、本当に「なぜ」の部分であって、「何」の部分ではないよ。その二つの境界はそんなに曖昧じゃない。読者がプログラミングの知識を完全に持っていて、外の世界については全く知らないと仮定してみて。コードがビットに対して何をするかに関するコメントが「何」で、コードが外の世界とどう関係しているかに関するコメントが「なぜ」だ。残りは好みや判断の問題だね。
GetNameMatchedTransfersWithin(int Days)は、何をするかは分かるけど、なんでそうするのかは分からない。もし誰かがそのメソッド名が長すぎると思うなら、正直洞窟に戻ってほしい。
「コメントが嘘になる」っていうのは、コメントにマジックナンバーがあるからだよ。もしコメントが「// 銀行振込マッチウィンドウの日数で等しい反対の金額を探してマッチさせる」だったら、実際のロジックが変わるまでそのコメントはずっと有効だよね。ロジックが変わったら…コメントを更新する?
> Uncle BobのextractNameMatchingTransfersWithinSettlementWindow()のアプローチは実際には役に立たないよ。 確かに役に立つよ。名前に関して「何」を特定したから、コメントは「なぜ」に集中できるしね。(ただ、XPの支持者の中には、決済ウィンドウをオブジェクトとしてモデル化しようとする人もいると思うけど…)
「tuple」は型と完全に重複してるけどね。
記事はコメントについてだけど、もっと一般的には、ここでの問題は物の名前付けについてだと思う。名前はアイデアを捉えるからね。何かに名前を付けないと、俺たち(少なくとも俺)はそれについて考えられない。名前が明確で説明的であればあるほど、その考えにその物を含めるための認知負荷が少なくて済む。TFAの例で「weight」が「w」よりも良い変数名だっていうのは、「weight」がすぐに意味を持つのに対し、「w」を使うと「wはweightだ」っていう面倒なことを考えなきゃいけないから。関数名も変数名と同じ目的を持ってるけど、データじゃなくて操作のためにね。もちろん、名前付けには文脈が重要で、関数を定義するとコード行が増えて複雑さが増す。冗長な変数名を定義するのも同じで、「weight」じゃなくて「the_weight_of_the_red_ball」とかね。だから、文脈を考慮したバランスが必要で、そのバランスを見つけるのにはある種のアートがあるかもしれない。コメントは、関数重視の「アンクルボブ」スタイルと、関数なしの「意識の流れ」スタイルの間の有用な中間を提供してくれるんだ。
あのフレーズはすごく誤解を招くと思う。私は「コメントは欠けている関連情報を追加すべき」という別のヒューリスティックを使ってるんだけど、これが私にはうまくいってる。冗長なコメントには逆効果だけど、「なぜ」の意味があいまいじゃないからね。コードが読者にとって変なことや予期しないことをしているかどうかも考慮に入れるような、もっと良いものがあるかもしれない。
この考え方は好きだけど、もう少し付け加えたいな。「コメントは、リファクタリングでは簡単に追加できない、欠けている関連情報を加えるべきだ」と。
「なぜを説明することが重要」というのは良い一般的なアドバイスだね。私からのコメントに関するさらなるアドバイスは、悪いコメントでも役に立つことがあるってこと(LLMの出力からのものは別かもしれないけど…)。だから、迷ったらコメントを書いた方がいいよ。自分の言葉で書いてね。2020年代の開発者体験を考えると、最後の文を追加する必要があった。LLMのコメントは、他の人間のコーダーに意味のある情報を伝えるべきものだから、あなたの人間の脳が考えられることは、たぶんもっと役立つ文脈になるよ。
俺は全然同意できないな。Claude Codeみたいなものでコードを生成してるなら、タスクについての重要なコンテキストがあるから、俺の経験上、すごく役立つ(ちょっと冗長だけど)コメントが出てくるよ。たまにそのコメントを編集したり書き直したりすることもあるけど(コード自体と同じように)、無コメントのコードを生成させるなんて絶対にしないよ。
LLMのコメントは、自分が道を見失わないようにするためのものだと思ってる。
AIのコメントは、何をどうするかの高レベルなまとめにはいいけど、なぜそれが必要なのかには失敗する。それが俺たちの出番だよ。
> 悪いコメントでも役に立つことがある 悪いコメントは、単にあまり役に立たないだけじゃなくて、しばしば有害だよ。コードベースで誤解を招くコメントに何度か遭遇すると(例えば、更新されてない、完全に間違ってる、用語の誤用で深く混乱する)、その嘘を無視してコードを直接読む方にシフトしちゃう。プログラミングを明確にするのに文章に頼るような思考の人は、メンテナンスや文章の明確さでも苦労することが多いからね。
ここから得た大きなポイントは、コメントや良い名前の目的は「このコードの文脈を理解する手助けをすること」だってこと。この記事では「コンテキストスイッチ」が何度も使われてて、実際には「コンテキスト」という言葉が他の使い方で出てこない。作者がフレンドリーな炎上を始めるって認めてるから、例のコードの最大の問題はオブジェクト指向で可変的なところだと思う。これが開発者に広がる文脈を強いるんだよね。replace()メソッドを読んだとき、引数がないからすぐに混乱した。stringToReplaceとalreadyReplacedはクラスのプロパティだから、その定義を探すには他のところ(つまり、関数の外)を見なきゃいけない。しかも、他の部分がそのプロパティをどう使ってるかもわからないし。これらの事実が、頭の中で持ち運ばなきゃいけない文脈を膨らませてる。なんでこれがクラスなの?replace()メソッドにはtranslate(symbolName)の呼び出しがあるけど、SymbolNameTranslatorクラスにtranslate()メソッドがないのはなんで?どっちが関数で使えるほどシンプルで、どっちがクラスにする必要があるって決めたの?SymbolReplacerも関数でできるはずだよ。これはイラスト用のコードだから、使い方や目的が明確じゃないのはわかるけど(元のボブ・マーチンの記事も役に立たないし)。SymbolReplacer.replace()を呼ぶたびに、半分置き換えられた文字列を使う理由があるの?もしあるなら、reduce関数を使ってすべての反復をリストで返すことで同じデータが得られるよ。単純で不変な関数は、その振る舞いの全範囲を含んでいるから、目的に関する余計な情報がないデータを受け取って返すだけなんだ。ほんとに一つのことしかやらない。
マーチン・ボブスタイルの関数からクラスへの変換は、文字通りスパゲッティコードになっちゃうね。
2000年代初頭からプログラミングをして学んだことの一つは、「一つのサイズが全てに合うアドバイス」なんて存在しないってこと。未来の人たちのために、僕が書いたコードをメンテナンスしなきゃいけない不幸な人たちに、ビジネスルールやコード/技術に関する仮定などの役立つヒントを提供しつつ、できるだけシンプルで明確なコードを書くのが大事だと思ってる。(自分のコードがシンプルで分かりやすいかどうかはどうやって判断するかって?ジュニアのチームメイトにレビューしてもらって、彼女/彼が10〜15分以上かかるところにはコメントを残してもらうんだ。)未来の人たちが、十分なコンテキストと超シンプルなコードを残しておいたことで、あまり僕を嫌いにならないことを願ってるよ。
僕は本気で、Uncle Bobが提案するように、ほとんどすべてを細かく切り分けてるよ。他の人たちが、関数やメソッドは20行以上にはならない方がいいって言うのをためらう中で、僕は自分のコードにそんなに長いものがあるなんて想像できない。でも、やり方は全然違うよ。マーチン氏は、彼のアイデアを実装して期待される利益を得るのがあまり得意じゃないみたいだ。根本的には、クラスを作るのがどれだけ複雑かを理解していないと思う。(特に、何か一貫性のあるものや直感的なものをモデル化しないときはね。でも、ジェフリーズの数独の経験が示すように、問題領域から解決領域に特に関連しないオブジェクトを誤ってモデル化する場合も同様だ。)パラメータに関する部分もナンセンスだよ。暗黙のthisオブジェクトから状態を引っ張るのは、明示的に渡すよりも明らかに悪いし、依存関係が減ったように見せかけてるだけだ。クリーンさの観点からも、thisオブジェクトの状態を変えるのは、パラメータを変えるよりも悪いし、もちろんそれは値を返すよりも悪い。これは、似たようなオブジェクトを繰り返し作るのに高いコストがかかる言語(Haskellファミリーじゃない言語とか)で最適化のために妥協するようなものだよ。単一行の関数については、通常は別の行にインライン化して、その結果に名前を付ける方が良いと感じてる。その値の名前は、関数名と同じくらい…価値があると思う。でも、例外も常にあるよね。
コメントや変数名に関しては、完全に変わり者だと思う。プロとしてコーディングしたことはないけど、15年くらいPythonと少しのJSを使ってる。変数名は長くて、何を表しているのか説明すべきだし、コメントも長くて、何が起こっているのかを説明すべきだと強く信じてる。なんで人々が「時間を節約」するために短いコメントや短い変数名を書こうとするのか全く理解できない。結局、変数名はCTRL+C、CTRL+Vで済むしね。短い名前はお互いに重なり合って、余計な複雑さやエラーの可能性を生むだけだと思う。1年か2年後に複雑なコードに戻った時、「three_tuple_of_weight_radius_price」みたいな変数名を使っておいて本当に良かったと思うよ。「tuple」や「t」なんて名前じゃなくてね。
決済ウィンドウ内っていうのは、メソッド名に加えるのはいいアイデアかも。少なくともググれるしね。
これにはすごく同意する。コードベースは、開発者同士の会話みたいなものだよね。次の人が自分よりもコードに不慣れな可能性は十分にあるし、深く潜ったばかりの自分が、重要だけどドキュメントが不十分なコードの行にたどり着くこともある。6ヶ月後の自分がその人になる可能性も高いしね。コミュニケーションの機会を大切にしよう。
> なんでみんな「時間を節約する」ために短いコメントや短い変数名を書くのか全然わからない。すごく長い変数名だと、演算子や呼び出しの間に余計なスペースができて、式の論理構造を理解しづらくなるんだよね。コードで `a + b` と書くとき、それは言語がそう要求するからその順番なんだ。慣れてるからその構文の言語を使ってるけど、実際には `+` が一番重要な部分だから、右の方に埋もれさせるべきじゃないと思う。変数名で型を示すのは好きじゃないな。`weight_radius_price` はいいけど(`wrp` よりはずっとマシだし、`tuple` や `t` よりも確実に良い)、a) 右辺の等号を見れば3タプルだってわかるし、b) 名前の構造から大体それを期待するっていうのは常識だし(ただ、クラスや構造の抽象が欠けてるかも?)、c) タプルを選ぶことは、他の3つの原子値をくっつける方法と比べると、文脈的にはあまり関係ないんじゃないかな。