| name | readable-code |
| description | コードを書く・レビューする・リファクタするときに適用する、可読性重視の コーディング規約。自明なコメントの排除と「目的(why)」コメント、ガード節による 早期リターン(正常系を浅いインデントへ)、意味のある命名(tmp/buf など汎用名の禁止)、 複雑なワンライナーの回避を扱う。Go / Python / Rust / TypeScript など言語を問わず適用する。 |
readable-code
コードの「意図」を最短で読み取れる状態を優先するための規約。
書くとき・レビューするとき・リファクタするときに、以下の原則を適用・指摘する。
これらは特定の流儀ではなく、広く知られた設計原則
(Fowler の Refactoring、McConnell の Code Complete、Clean Code、Cognitive Complexity)
に沿ったもの。末尾の参考に一次情報を挙げる。
使いどころ
- 新しく関数・メソッドを書くとき
- 既存コードをレビュー / リファクタするとき
- 「コメントを足そうとした」「インデントが深くなってきた」「名前に迷った」瞬間
原則1: 自明なコメントを書かない
コードを読めば分かることをコメントで繰り返さない。実装を更新したときにコメントの更新が
漏れると、コードとコメントが矛盾し、無いより有害になる (stale comment)。
インラインコメントを書きたくなったら、それは次のどちらかのサイン:
- 変数・関数名が不明瞭 → 名前を直す
- 処理のかたまりに名前を付けたい → 関数に抽出する
i++
if x > 0 {
total = total + x*rate
}
i++
if hasPositiveBalance(x) {
total = addTransactionFee(total, x, rate)
}
コメントを消すこと自体が目的ではない。「コメントで補う」代わりに
「名前と構造で自明にする」ことが目的。
原則2: コメントは「目的 (why)」を書く
コメントを残すなら、その処理が何をしているか (what) ではなく、
なぜそうするのか / 何を目的としているのか (why) を書く。
関数レベルのドキュメントで、処理の目的・背景・制約・トレードオフを説明する。
whatはコードが語る。whyはコードに書けない — だからコメントの出番はそこだけ。
while retries < 3:
...
MAX_PAYMENT_RETRIES = 3
while retries < MAX_PAYMENT_RETRIES:
...
残す価値があるコメントの例:
- 意図・目的 (この関数は何のために存在するか)
- 非自明な判断の理由 (なぜこのアルゴリズム / この定数 / この順序か)
- 仕様・外部制約・既知の落とし穴 (「APIが○○を返す前提」「先に△△しないと壊れる」)
- 公開 API のドキュメント (godoc / docstring / JSDoc)
原則3: ガード節で早期リターン / continue する
エラーケース・前提を満たさないケースは、関数の入口で早期リターンして閉じる。
ループ内なら continue で早期に打ち切る。異常系を先に処理しきると、以降は
「正常系だけを考えればよい」状態になり、読み手の負荷が下がる。
func process(order *Order) error {
if order != nil {
if order.Paid {
if len(order.Items) > 0 {
return ship(order)
} else {
return errors.New("no items")
}
} else {
return errors.New("not paid")
}
} else {
return errors.New("nil order")
}
}
func process(order *Order) error {
if order == nil {
return errors.New("nil order")
}
if !order.Paid {
return errors.New("not paid")
}
if len(order.Items) == 0 {
return errors.New("no items")
}
return ship(order)
}
ループでも同じ:
for user in users:
if user.is_disabled:
continue
if not user.email:
continue
send_newsletter(user)
原則4: 正常系は浅いインデントに置く
原則3の帰結。if status == "OK" の then 側で本処理をしない。
代わりに if status != "OK" で早期リターンし、本当に行いたい処理を
関数スコープの浅い位置に集約する。読みたいコードの意図が一番浅い所に並ぶ。
if status == "OK" {
result := doTheRealWork()
save(result)
notify(result)
}
return
if status != "OK" {
return
}
result := doTheRealWork()
save(result)
notify(result)
例外: switch 的な if / elif / elif
分岐が「異常系 vs 正常系」ではなく、同格の複数ケースの振り分け
(いわゆる switch 相当) の場合は、無理に早期リターンへ潰さない。
むしろ意図が「多方向分岐」であることが伝わる形にする。
match kind:
case "circle":
return circle_area(r)
case "square":
return square_area(r)
case "triangle":
return triangle_area(r)
case _:
raise ValueError(f"unknown kind: {kind}")
原則5: 複雑なワンライナーを避ける
1行に詰め込んだ賢いコードは、書くのは一度・読むのは何度も、という非対称のコスト。
三項演算子の入れ子、長すぎるメソッドチェーン、副作用を織り込んだ内包表記などは、
適切な変数名を挟んで段階に分ける。行数は増えても認知負荷は下がる
(cognitive complexity の低減)。
const r = u && u.roles && u.roles.find(x => x.a && x.lvl > (u.vip ? 2 : 5)) ? "y" : "n";
const requiredLevel = user?.vip ? 2 : 5;
const hasQualifyingRole = (user?.roles ?? [])
.some(role => role.active && role.level > requiredLevel);
const result = hasQualifyingRole ? "y" : "n";
短さ (code golf) は目的ではない。目的は「意図が最短で伝わること」。
原則6: 意味のある名前をつける (tmp / buf を避ける)
tmp temp buf data val flag x のような汎用名は、その変数が
何であるかを隠す。名前と構造に意味を持たせる。McConnell の
「1変数=1用途」原則に従い、使い回しもしない。
tmp := getUser(id)
buf := tmp.Orders
data := 0
for _, x := range buf {
data += x.Price
}
user := getUser(id)
orders := user.Orders
totalPrice := 0
for _, order := range orders {
totalPrice += order.Price
}
- ドメインの語彙で、発音でき検索できる名前にする
- 一時変数でも意味のある名前を付ける (「一時的」は用途であって名前ではない)
- 例外的に許容: ごく狭いスコープの慣習的イテレータ (
i, j)、
数学的慣習 (x, y 座標) など、意味が自明な場合のみ
レビュー時チェックリスト
コードを書き終えたら / レビューするときに、この観点で見直す:
やりすぎ注意 (アンチパターン)
- ガード節の乱立: 早期リターンが 5 個も 6 個も並ぶなら、関数が多くの責務を
抱えているサイン。分割やバリデーションの前段への切り出しを検討する。
- コメント全消し: 原則1は「自明なコメント」を消すもの。目的・理由・制約
(原則2) まで消してはいけない。
- 省略しすぎた名前: 意味を込めるあまり
theUserWhoJustLoggedInToTheSystem の
ように冗長化しない。スコープが狭いほど短くてよい。
参考