なぜコメントを書くか
将来の開発者がより簡単に、かつより正確に、コードを利用・メンテナンスできるようにするためです。
仕事で書くほとんどのコードは将来の変更・メンテナンスが必要です。したがって仕事で書くほとんどのコードには適切なコメントが必要です。例外はメンテナンスしないコード、すなわちハッカソンやプロトタイプ目的で書いた、捨てるつもりのコードです。
コメントに何を書くか
この記事のタイトルにもしていますが、これは A Philosophy of Software Design という本に書かれているものです(13章のタイトルになっています)。
Comments should describe things that aren’t obvious from the code.
「コメントにはコードを見てもパッとわからないことを書こう」
コードを見てパッとわかることはコードを見ればいいわけです。コード外のコンテキストを知らないとわからないことや、コードを時間と労力をかけて読み込まないとわからないことを、コメントに書こうということです。
以降の内容もこの本に書かれていることを参考に、実際に仕事で書いた具体例を示しながら説明します。
具体例
Interface comments / Summary doc
そのコンポーネント(クラス、モジュール、メソッド、関数、etc)が何をするものなのか、どういった動きをするのかを「直感的に」理解できる情報を書きましょう。これらの情報は、コメントやドキュメントがない限り、コンポーネントの中の実装や使われている箇所を時間をかけて読まないとわからない(パッとはわからない)ものです。
書く場所は、コンポーネントの定義の上です。
Tips としては、以下のようなものがあります。
- 実装がどうなっているかよりも、一段階抽象化した情報を提供するようにする。
- 利用時の注意点なども書いておくとよい。
- インターフェイスとなるメソッド・関数などは、引数や返り値の説明(型や補足情報)などもあるとよい。
これがあると、コンポーネントが何をするものなのかを知るために、コンポーネントの中の実装や、それが使われている場所を長時間かけて読まなくてよくなります。実装者以外がコンポーネントをはじめて読むときにまず知りたいのは、たいていの場合、そのコンポーネントの実装がどうなっているかではなく、そのコンポーネントが何をするものなのかです。それが手間をかけずにわかる状態になっているのが理想です。
例
- クラス
#
# ベースプラン契約が終了したときに残っているオプション契約の終了処理を行う。
#
# 指定された日付の前日に契約が終了したベースプランの契約について、同じ会社のオプション契約を status に応じて以下のように処理する。
# - まだ開始していないものは、契約をキャンセルする。
# - 開始して走っているものは、契約の終了日をベースプランの終了日と同じ日に変更して終了させる。同時に自動更新設定もオフにする。
# - すでに終了しているものは、何も行わない。
#
# @see https://github.com/wantedly/company-payment/blob/master/doc/specs/base_plan_termination.md
#
# このサービスはスケジューラーで実行されて、途中で失敗したら再実行する必要があるので、このサービスの処理は冪等になっている必要がある。
#
class OptionContractsTerminationService
def self.perform(date: Date.current)
...
end
- モデル
まずはそのモデルは何をするモデルなのかが数行でわかるように書くとよいです。また、レコードがどういったタイミングでどのようにして作られるか、どういったタイミングで状態が変わるかといった、ライフサイクルについても書いてあげると、振る舞いがイメージしやすくなります。
# == Table Explanation
#
# Plan プラン
#
# プラン情報を持っているマスターテーブル(会社ごとに作られない)。
# このテーブルのレコードを作るのは社内の人間(開発者 + オペレーションメンバー)のみ。
# 同じ "プラン" (e.g. "プレミアムプラン") でも複数の期間で提供している (e.g. 3ヶ月、6ヶ月、12ヶ月など) 場合、
# 期間ごとに別のレコードを作る。
#
# この Plan が持っている金額や自動更新するかどうかなどの情報はそのプランのデフォルト値で、
# Contract レコードで契約単位で値をカスタマイズすることもできる。
#
# プランの値上げ・値下げなどを行う場合は、既存のレコードの値を変更するのではなく、別のレコードを作ること。
# 既存のレコードの値を変えると、過去の売上計算の結果が正しかったかどうかわからなくなるため。
#
# == Columns
#
# id
# name プラン名。企業向けの請求書やメール、画面等に表示される。
# code プランコード。システムで申し込みなどのときにプランを特定する用途。
# ...
#
class Plan < ApplicationRecord
- ユーティリティ系のもの
何ができるものなのか、オプション、使用方法などを書いてあげるとわかりやすいです。
# == Summary
#
# This custom validator validates an attribute of time-related type (datetime and date)
# is after another attribute in terms of time order.
#
# This is useful when you have, for example, a pair of columns composing a term,
# like `start_on` and `end_on`, where `end_on` must always be after `start_on`.
#
# == Usage
#
# class Contract < ApplicationRecord
# validates :end_on, time_order: { after: :start_on, allow_nil: true }
# end
#
# == Options
#
# after [String, Symbol] Specify the name of another attribute to compare to.
# allow_nil [Boolean] When true, it skips validation if either the attribute
# or the compared attribute is nil. Default: false.
#
class TimeOrderValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
...
- 関数・メソッド
引数、返り値の説明の他に、どういうものを受け取ったらどういうものを返すかの例も書いてあげると、何をするものなのか理解しやすくなることがある。
# 契約金額に割引を適用して月数で按分したものを返す。
#
# @param amount [Integer] 契約金額
# @param amount_off [Integer] 固定額割引額
# @param percent_off [Float] %割引率。20%オフなら 0.2 を渡す。
# @param months [Integer] 月数
#
# @return [Array<Integer>]
# @example
# (amount: 120000, amount_off: 0, percent_off: 0.0, months: 6) => [20000, 20000, 20000, 20000, 20000, 20000]
# (amount: 120000, amount_off: 30000, percent_off: 0.0, months: 6) => [15000, 15000, 15000, 15000, 15000, 15000]
# (amount: 120000, amount_off: 0, percent_off: 0.2, months: 6) => [16000, 16000, 16000, 16000, 16000, 16000]
#
def calc_monthly_tax_excluded_revenue_amounts(amount:, amount_off: 0, percent_off: 0.0, months:)
...
end
Data structure members annotation
テーブルのカラムやオブジェクトのフィールド、enum の値、定数などについて、それが何なのか、どういったコンテキストで使われるのかなどを記述します。書く場所は宣言している場所の隣か上です。
こうしたものは名前が簡素だったりして、何を意味しているのかやどういった文脈で使われるのかが名前だけからだと想像しにくいことがよくあります。コメントが書かれていると、それを理解するために使われている箇所を調べて回るといったことをしなくてよくなります。また、あるカラムがいつの間にか意図とは違った別の用途でも使われるようになってしまったみたいなことを減らすこともできます。
テーブルのカラムや enum などは歴史的経緯で今は使われなくなっていたり、非推奨になっているものがあることがあります。そういったものには ”Obsolete” や "Deprecated” などと書いておくとよいです。
例
- テーブルのカラム
上で説明したモデルの summary doc と一緒に書くようにしています。
# == Table Explanation
#
# Plan プラン
#
# 商品情報を持っているマスターテーブル(会社ごとに作られない)。
# ...
#
#
# == Columns
# * [C] と書いてあるものは Contract 作成時に初期値として Plan の値がコピーされる。
#
# id
# name [C] プラン名。企業向けの請求書やメール、画面等に表示される。
# code プランコード。システムで申し込みなどのときにプランを特定する用途。
# - オペレーションメンバーが作成したものなど、システムで申し込みが行われないものには存在しない。
# category 基本プランなら base、オプションなら option。
# available_from 提供可能期間の開始日。
# available_to 提供可能期間の終了日。まだ提供終了の予定がない場合は null。
# auto_renew [C] 契約を自動更新するかどうか。
# term_count [C] 契約期間の数字部分。6ヶ月なら 6。
# term_unit [C] 契約期間の単位部分。6ヶ月なら month。
# currency [C] 通貨。
# amount [C] 金額(総額、税抜)。月10万円の6ヶ月プランなら 600000。
# note 社内用メモ。企業には表示されない。
#
# created_at
# updated_at
# deleted_at
#
class Plan < ApplicationRecord
- Enum
class Post < ApplicationRecord
enum category: {
blog: 0, # [DEPRECATED] かつてあった「チーム」機能の投稿の一種
feed: 1, # 旧「会社フィード」の名残でシンプルなプレーンテキスト形式のもの。今も会社ページに細々と投稿導線が存在する。
employee_interview: 2, # [DEPRECATED] かつて「社員インタビュー」というものがあったが、2017年に post_article に移行・統合された. https://github.com/wantedly/wantedly/issues/xxxxx
experience_story: 3, # 「インターン体験記」
post_article: 4, # 「ストーリー」の記事
}
- 変数
class Ticket < ApplicationRecord
LIMIT_PER_PURCHASE = 5 # 1ユーザーが一度に購入可能な枚数の上限
MONTHLY_LIMIT_PER_USER = 20 # 1ユーザーが1ヶ月に購入可能な枚数の上限
MIN_DAYS_TO_EXPIRATION_FOR_SALE = 30 # 有効期限までの残り日数がこれより短いものは販売しない
Implementation comments
実装の中でコードを見てもパッとわからないことを書きます。書く場所はメソッドの中の実装コードの横や上です。
How よりも、What や Why を書くように意識しましょう。何をやりたいか(What)がわかれば、コードが何をどう処理しているのか(How)の理解は簡単になります。またコードがどう動くのかがわかっても、なぜそれがやりたいのか (Why) はより広い範囲のコンテキストを知らないとわからないこともあります。
このコメントがあると、コードの意図を簡単にかつ正しく理解できるようになります。将来の開発者が「この処理要らなくない?」と思って消してしまい事故が起きるといったことも防ぐことができます。
本番でエラーが起きてそれに対処するコードを入れたときなどは、その issue や PR の URL を貼っておくとよいです。
例
- なぜそのようなコードが書かれているのかを補う情報 (Why) を書く。
.modal-container {
/* Header の z-index よりも高くする必要がある */
/* FIXME: Header の z-index をなくしたい */
z-index: 2;
}
null や 0 など特殊な値を取るケースをハンドリングするコードを書くときは、どういうときにそうした値を取るのかを書いておくと理解しやすくなります。
# 按分月数が 0 になるのは、monthly プランで、契約開始後に契約自体がキャンセルされた場合。
# このときは、当月に計上する通常項目はなく、過去に計上済みの項目があればその消し込み項目を出力する。
if dividing_months == 0
cancel_past_items_if_necessary!(contract, year, month, dividing_months)
return
end
- この先の数行で何をやりたいか (What) を書く
class TicketCollection
def consume!(amount)
# 以下の優先度でチケットを消費していく
# 1. 有効期限が近い順 (有効期限のないものは最後)
# 2. 有効期限が同じであれば、ロットの残り枚数の少ない順
tickets = tickets.group_by { |t| t.expires_on.present? }
ordered_tickets = []
ordered_tickets += tickets[true].to_a.sort_by { |t| [t.expires_on, t.remaining_amount] }
ordered_tickets += tickets[false].to_a.sort_by(&:remaining_amount)
ordered_tickets.each do |ticket|
...
end
PRレビューのタイミングはコメントを追加するいい機会
コードが書けて PR を作ったら、レビュワーに request review する前に、一度自分で File Changes を見てセルフレビューをしましょう。文脈を知らない人の気持になって、ここ何やってるか/なんでやってるかパッと見てもわからないなと思ったら、コードコメントを追加しましょう。こういうとき、PR上にコメントを書くこともあると思いますが、PR上に書くよりもコード自体に書いてしまった方が、後の開発者も目にしやすくなるので有益です。
また、レビュワーに「これってなんで必要ですか?」「ここのコードの意図はなんですか?」みたいに聞かれたら、それは他の人が見てパッとわからなかったということなので、PR上で返答するだけでなく、コードコメントにも書いておきましょう。
終わりに
仕事で書くコードというのは基本的に自分だけでなく他の人も読んでメンテナンスするものであり、自分の書いたコードを自分でずっと面倒を見続けられるわけでもありません。自分の手を離れてもそのコードを他の人が無理なくメンテナンスできるように、親切なコメントを残すことを心がけましょう。