読まれるコードを書く
読了目安 40 分
- コメントに書くべきことと書かないことを区別できる
- レビューで何を見るべきか説明できる
- ログに何を出すべきか判断できる
コードは書かれる時間より、読まれる時間の方が圧倒的に長いです。
あなたが1時間で書いたコードは、その後の数年間、何十回も読まれます。 レビュアーが読み、半年後のあなたが読み、障害対応中の誰かが深夜に読みます。
この章は、その読者に向けてどう書くかの話です。 そしてこれは、レビューで指摘される内容そのものでもあります。
名前
名前は最も安いドキュメント
// 何が入っているか分からない
const d = new Date();
const list = await fetch(...);
function check(x) { ... }
// 読めば分かる
const publishedAt = new Date();
const pendingOrders = await fetch(...);
function isEligibleForDiscount(order) { ... }コメントを1行書くより、名前を3文字長くする方が効果があります。 コメントは古くなりますが、名前はコードと一緒に動きます。
具体的な指針
| 対象 | 指針 | 例 |
|---|---|---|
| 変数 | 中身が分かる名詞 | pendingOrders(data ではなく) |
| 真偽値 | is has can で始める | isPublished hasPermission |
| 関数 | 動詞から始める | calculateTotal fetchUser |
| 定数 | 値の意味を書く | MAX_RETRY_COUNT(3 を直接書かない) |
単位と型を名前に入れる
// 3600 は秒? ミリ秒? 分?
const timeout = 3600;
// 迷わない
const timeoutSeconds = 3600;
const timeoutMs = 3_600_000;単位の取り違えは実際に事故になります。 名前に入れるのが最も安い対策です。
それは名前の問題ではなく、その関数が複数のことをしているサインです。
processData のような名前しか付けられないなら、
中身が「検証して、変換して、保存する」の3つをやっている可能性が高いです。
分割すれば validateOrder toRecord saveOrder と自然に名付けられます。
関数
小さく保つ
目安は画面に収まる(50行以内)、引数は3つ以内、ネストは3段以内です。 厳密な数字より、「一度に理解できる量か」が基準です。
早期リターンでネストを減らす
// ネストが深い
function apply(user, coupon) {
if (user !== null) {
if (user.isActive) {
if (coupon.isValid) {
return discount(user, coupon);
} else {
throw new Error('クーポンが無効です');
}
} else {
throw new Error('アカウントが停止中です');
}
} else {
throw new Error('ユーザーが見つかりません');
}
}
// 例外を先に片付ける
function apply(user, coupon) {
if (user === null) throw new Error('ユーザーが見つかりません');
if (!user.isActive) throw new Error('アカウントが停止中です');
if (!coupon.isValid) throw new Error('クーポンが無効です');
return discount(user, coupon);
}異常系を先に潰して、正常系を最後に平坦に書く。 これだけで読みやすさが変わります。
引数が増えたらオブジェクトにする
// 呼び出し側で順番を間違える
createOrder(userId, itemId, 2, true, false, null);
// 何を渡しているか読める
createOrder({
userId,
itemId,
quantity: 2,
isGift: true,
useCoupon: false,
});特に真偽値の引数が2つ以上並ぶと、呼び出し側が必ず読めなくなります。
コメント
WHY を書く。WHAT は書かない
// 悪い: コードを読めば分かることを繰り返している
// ユーザーIDでユーザーを取得する
const user = await getUser(userId);
// 良い: なぜそうしたかが書いてある
// 決済APIが2秒でタイムアウトするため、それより短く設定している
const timeoutMs = 1500;コードは何をしているかを語れますが、なぜそうしたかは語れません。 そこだけをコメントに書きます。
書く価値があるのは主にこの3つです。
| 種類 | 例 |
|---|---|
| 自明でない制約 | 「外部APIの仕様上、1秒間に3回までしか呼べない」 |
| 回避した罠 | 「Safari でこのイベントが2回発火するため、フラグで抑止している」 |
| 選ばなかった代替案 | 「Map の方が速いが、順序が必要なので配列にしている」 |
コードを直してコメントを直し忘れると、嘘の情報が残ります。 これは何も書いていないより有害です。
だから「コードで表現できることはコードで表現する」が優先です。 コメントを増やす前に、まず名前と分割を疑ってください。
TODO の書き方
// 悪い: 誰がいつやるのか分からず、永久に残る
// TODO: あとで直す
// 良い: 追跡できる
// TODO(PROJ-123): 決済APIのv2移行時にリトライ処理を削除するエラーの扱い
握り潰さない
// 最悪: 何が起きたか永久に分からなくなる
try {
await savePayment(order);
} catch (e) {
// 握り潰し
}
// 悪い: ログはあるが、呼び出し側は成功したと思っている
try {
await savePayment(order);
} catch (e) {
console.log(e);
}
// 良い: 記録して、呼び出し側に伝える
try {
await savePayment(order);
} catch (error) {
logger.error('決済の保存に失敗', { orderId: order.id, error });
throw new PaymentSaveError(order.id, { cause: error });
}握り潰されたエラーは、障害調査で最も憎まれる存在です。 「エラーは出ていないのに動いていない」という最悪の状態を作ります。
情報を足して投げ直す
エラーを再送出する時は、どの処理で失敗したかの情報を足します。
cause を使えば元のエラーも辿れます。
ログ
ログは「動かすため」ではなく、後から調べるために書きます。 実装中は全部分かっているので不要に思えますが、 3ヶ月後の自分と、深夜に障害対応する誰かのために書いてください。
構造化ログ
// 検索しづらい
logger.info(`user ${userId} ordered item ${itemId}`);
// 絞り込み・集計ができる
logger.info('order created', { userId, itemId, amount, orderId });文字列を繋げたログは、後から userId で絞り込めません。
キーと値の形で出すのが実務の標準です。
何を出すか
| レベル | 使う場面 |
|---|---|
error | 対応が必要な異常。アラートに繋がる |
warn | 異常ではないが注視したいこと(リトライした、上限に近い) |
info | 業務上の重要イベント(注文作成、決済完了) |
debug | 開発時の詳細。本番では出さない |
本当は対応不要なものを error で出すと、アラートが鳴り続け、
やがて誰も見なくなります(アラート疲れ)。
そうなると、本物の障害も見逃されます。
error は「人が起きて対応すべきか」で判断してください。
第17章の再掲です。ログは長期保存され、多くの人が見られます。 パスワード・トークン・カード番号・メールアドレスは出さないでください。
テスト
何をテストするか
全部をテストするのではなく、壊れたら困るところを優先します。
- 仕様の中心(金額計算、権限判定、状態遷移)
- 境界値(0件、1件、上限、空文字、null)
- 一度壊れたところ(バグ修正の際に、再発防止のテストを足す)
AAA パターン
テストは3段に分けて書くと読みやすくなります。
test('クーポンを適用すると10%引きになる', () => {
// Arrange(準備)
const order = createOrder({ subtotal: 1000 });
const coupon = createCoupon({ rate: 0.1 });
// Act(実行)
const result = applyCoupon(order, coupon);
// Assert(検証)
expect(result.total).toBe(900);
});テスト名は日本語でよい
// これが何をテストしているか、名前だけで分かる
test('在庫が0の商品はカートに追加できない', ...)
test('同じクーポンは2回使えない', ...)テスト名は仕様書になります。 実装より先にテスト名を並べると、 考慮漏れに気づけます。
外部依存を全部モックにすると、「テストは通るが本番で動かない」状態になります。
モックにすべきなのは、外部サービス・時刻・乱数のような 自分で制御できないものだけです。自分のコードはできるだけ本物を使ってください。
コードレビュー
見る観点
レビュアーとして見るなら、この順番です。
- 仕様を満たしているか — そもそも作るものが合っているか
- 壊れる入力はないか — null、0件、境界値、同時実行
- セキュリティ — 第17章のチェックリスト
- 読めるか — 名前、分割、コメント
- 好みの問題(最後。指摘するなら
nit:を付ける)
1と2が最優先です。変数名の指摘に3件コメントして、 認可漏れを見逃したら意味がありません。
指摘する側の作法
悪い: 「これはダメ」
良い: 「ここで items が空だと落ちませんか?」
悪い: 「なんでこう書いたの」
良い: 「Map を使う案もありそうですが、配列にした理由はありますか?」
- 人ではなくコードに向ける
- 理由を書く(「なぜダメか」が分からないと次も同じことをする)
- 重要度を示す(
nit:must:question:を付けると伝わりやすい) - 良いところも書く(新人は特に、褒められないと不安になります)
指摘される側の作法
第5章でも触れましたが、大事なので繰り返します。
- 指摘はコードへの評価であって、人格への評価ではありません
- 納得できなければ議論していい。ただし理由を添えて
- 理由が分からないまま直すと、次も同じ指摘を受けます
運用を意識する
新人が最初は意識しないけれど、実務で最も差が出る観点です。
| 観点 | 具体的に |
|---|---|
| 失敗する前提 | 外部呼び出しは失敗する。タイムアウトとリトライを設計する |
| 冪等性 | リトライされても壊れないか |
| 観測できるか | 障害時に原因を特定できるログ・メトリクスがあるか |
| 戻せるか | 問題が起きた時にロールバックできるか |
// 動くけれど、本番では怖い
const result = await externalApi.call(payload);
// 失敗と遅延を織り込んでいる
const result = await withTimeout(
() => externalApi.call(payload),
{ timeoutMs: 1500, retries: 2, onRetry: (n) => logger.warn('retrying', { n }) },
);コードを書き終えたら、この問いを自分に投げてください。
答えられなければ、ログが足りていません。 これを習慣にするだけで、コードの質が一段上がります。
実務の落とし穴まとめ
- エラーの握り潰し — 「エラーは出ていないのに動かない」を作る
- コメントの腐敗 — 直し忘れた嘘のコメントは、無いより有害
datalisttmp— 中身が分からない名前- 真偽値の引数が並ぶ — 呼び出し側が読めない
- error レベルの乱用 — アラート疲れで本物を見逃す
- モックだらけのテスト — 通るのに本番で動かない
まとめ
- コードは書く時間より読まれる時間の方が長い
- 名前は最も安いドキュメント。コメントより先に名前を直す
- 異常系を先に潰し、正常系を平坦に書く(早期リターン)
- コメントには WHY を書く。WHAT はコードが語る
- エラーを握り潰さない。記録して、情報を足して投げ直す
- ログは後から調べるため。構造化して出す。個人情報は出さない
- テスト名は仕様書。壊れたら困るところから書く
- レビューは仕様とセキュリティが最優先、名前は最後
- 「本番で落ちたらどう調べる?」を書き終わるたびに自問する
章末問題
レビューで PR を見る時、最も優先して確認すべきことは?
外部APIの呼び出しを try-catch で囲み、catch で console.log(e) だけしています。何が問題ですか。
ログの出し方として最も適切なのは?
これで第6部は終わりです。
ここまでで、道具(第1部)・進め方(第2部)・仕組み(第3部)・安全と品質(第6部) が揃いました。 残りの第4部・第5部では、実際に使う言語と、サービスを構成する技術を扱います。