プログラマのための IT 教科書
第6部 安全と品質

読まれるコードを書く

読了目安 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文字長くする方が効果があります。 コメントは古くなりますが、名前はコードと一緒に動きます。

具体的な指針

対象指針
変数中身が分かる名詞pendingOrdersdata ではなく)
真偽値is has can で始めるisPublished hasPermission
関数動詞から始めるcalculateTotal fetchUser
定数値の意味を書くMAX_RETRY_COUNT3 を直接書かない)

単位と型を名前に入れる

// 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 で出すと、アラートが鳴り続け、 やがて誰も見なくなります(アラート疲れ)。

そうなると、本物の障害も見逃されます。 error は「人が起きて対応すべきか」で判断してください。

ログに個人情報を出さない

第17章の再掲です。ログは長期保存され、多くの人が見られます。 パスワード・トークン・カード番号・メールアドレスは出さないでください。

テスト

何をテストするか

全部をテストするのではなく、壊れたら困るところを優先します。

  1. 仕様の中心(金額計算、権限判定、状態遷移)
  2. 境界値(0件、1件、上限、空文字、null)
  3. 一度壊れたところ(バグ修正の際に、再発防止のテストを足す)

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回使えない', ...)

テスト名は仕様書になります。 実装より先にテスト名を並べると、 考慮漏れに気づけます。

モックを使いすぎない

外部依存を全部モックにすると、「テストは通るが本番で動かない」状態になります。

モックにすべきなのは、外部サービス・時刻・乱数のような 自分で制御できないものだけです。自分のコードはできるだけ本物を使ってください。

コードレビュー

見る観点

レビュアーとして見るなら、この順番です。

  1. 仕様を満たしているか — そもそも作るものが合っているか
  2. 壊れる入力はないか — null、0件、境界値、同時実行
  3. セキュリティ — 第17章のチェックリスト
  4. 読めるか — 名前、分割、コメント
  5. 好みの問題(最後。指摘するなら 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 }) },
);
「これ、本番で落ちたらどう調べる?」

コードを書き終えたら、この問いを自分に投げてください。

答えられなければ、ログが足りていません。 これを習慣にするだけで、コードの質が一段上がります。

実務の落とし穴まとめ

  1. エラーの握り潰し — 「エラーは出ていないのに動かない」を作る
  2. コメントの腐敗 — 直し忘れた嘘のコメントは、無いより有害
  3. data list tmp — 中身が分からない名前
  4. 真偽値の引数が並ぶ — 呼び出し側が読めない
  5. error レベルの乱用 — アラート疲れで本物を見逃す
  6. モックだらけのテスト — 通るのに本番で動かない

まとめ

  • コードは書く時間より読まれる時間の方が長い
  • 名前は最も安いドキュメント。コメントより先に名前を直す
  • 異常系を先に潰し、正常系を平坦に書く(早期リターン)
  • コメントには WHY を書く。WHAT はコードが語る
  • エラーを握り潰さない。記録して、情報を足して投げ直す
  • ログは後から調べるため。構造化して出す。個人情報は出さない
  • テスト名は仕様書。壊れたら困るところから書く
  • レビューは仕様とセキュリティが最優先、名前は最後
  • 本番で落ちたらどう調べる?」を書き終わるたびに自問する

章末問題

レビューで PR を見る時、最も優先して確認すべきことは?

外部APIの呼び出しを try-catch で囲み、catch で console.log(e) だけしています。何が問題ですか。

ログの出し方として最も適切なのは?

これで第6部は終わりです。

ここまでで、道具(第1部)・進め方(第2部)・仕組み(第3部)・安全と品質(第6部) が揃いました。 残りの第4部・第5部では、実際に使う言語と、サービスを構成する技術を扱います。

読み終わったら記録しておくと、目次で進み具合が分かります。