
リーダブルコードを読んでみてのあらためての要約ガイド
はじめに
IT/DEV事業部でエンジニアをしているSakataです!
今回は、これからコードを学ぶ方や、普段コードを書いているけれどなんとなく読みにくいコードになっているな、という方に向けて、リーダブルコードでの要約をまとめました。
いろいろなプロジェクトをみてきて、読みやすいコードとそうでないコードではレビューの負荷が大きく変わると感じています。
読みやすいコードは他の人ためだけでなく、半年後や期間の開いた後の自分自身のためにもつながるためになる。
ちょっとした工夫でコードは劇的に読みやすくなるものなので、今回はリーダブルコードの中で出てくるポイントをTypeScriptベースでのまとめを作成してみました。
リーダブルコードは英語版であればpdfで無料でも読めます!
https://mcusoft.wordpress.com/wp-content/uploads/2015/04/the-art-of-readable-code.pdf
基本定理
コードは他の人が最短時間で理解できるように書くべきである。
— The Fundamental Theorem of Readability
ここでの「他の人」には半年後の自分も含まれる。「短いコード」や「行数の少ないコード」が必ずしも読みやすいわけではない。理解に要する時間を基準にする。
1// 短いが理解しにくい2const r = xs.reduce((a, x) => (x.s === 'a' ? a + x.v : a), 0);34// 長いが理解しやすい5const activeItemsTotal = items6 .filter((item) => item.status === 'active')7 .reduce((sum, item) => sum + item.value, 0);
2. 名前に情報を詰め込む(Ch.2)
明確な単語を選ぶ
汎用的すぎる名前は情報がない。動作を正確に表す動詞を選ぶ。
get
-> fetch / download / find / resolve
send
-> deliver / dispatch / enqueue / publish
make
-> create / build / generate / compose
do / handle / process
-> 具体的な動詞に置き換える
data / info
-> 何の data なのかを名前に含める
tmp / temp
-> 短いスコープでのみ許容
1// NG2function getPage(url: string): Promise<string> { ... }34// OK: 「HTTP で取得する」ことが名前からわかる5function fetchPageContent(url: string): Promise<string> { ... }
具体的な名前 > 抽象的な名前
1// NG: run で何を走らせるのかわからない2function run(): void { ... }34// OK: 何をするか具体的5function pollOutboxAndDispatchJobs(): void { ... }
名前に追加情報を付ける
単位・状態・形式が重要なら名前に含める。
1// NG: 単位がわからない2const timeout = 300;3const size = 1024;45// OK: 単位が名前からわかる6const timeoutMs = 300;7const fileSizeBytes = 1024;89// NG: エンコード済みかどうかわからない10const url = userInput;1112// OK: 状態が名前から明確13const rawUrl = userInput;14const sanitizedUrl = encodeURI(rawUrl);15
名前の長さはスコープに比例させる
1// OK: 狭いスコープでは短い名前2const ids = workspaces.map((ws) => ws.id);34// OK: 広いスコープ(モジュール export)では長く正確な名前5export function resolveWorkspaceScopeForAccount(6 accountId: AccountId,7 workspaceId: WorkspaceId,8): Promise<WorkspaceScope> { ... }
頭文字・省略語のルール
チーム内で確立した略語のみ使用可: ws(workspace)、tx(transaction)、db(database)
初見で意味がわからない略語は禁止: wm、ca、bwe
3. 誤解されない名前(Ch.3)
限界値を表す名前
上限(含む)
推奨命名:maxXxx
例:maxRetryCount = 3(3 回目まで OK)
下限(含む)
推奨命名:minXxx
例:minPasswordLength = 8(8 文字以上)
範囲の始まり(含む)
推奨命名:firstXxx
例:firstValidIndex = 0
範囲の終わり(含まない)
推奨命名:endXxx
例:endIndex = 10(0〜9)
範囲の終わり(含む)
推奨命名:lastXxx
例:lastPage = 5
Boolean の命名
is / has / can / should で始めると、true/false の意味が読める。
1// NG: 否定形が混乱を招く2const disableAuth = false; // false = 有効?無効?34// OK: 肯定形5const authEnabled = true;67// NG: 名前の意味が曖昧8const valid = checkWorkspace(ws);910// OK: true が何を意味するか明確11const isWorkspaceOperable = ws.status === 'active';12
filter / select / exclude の使い分け
filter だけでは「含む」「除外する」どちらか曖昧になりやすい。
1// 曖昧: deleted を含む?除外する?2const filtered = filterByStatus(workspaces, 'deleted');345// 明確6const withoutDeleted = excludeDeletedWorkspaces(workspaces);7const activeOnly = selectActiveWorkspaces(workspaces);
4. 美しさ(Ch.4)
一貫したレイアウトパターン
読み手が「パターン」を認識できれば、構造を瞬時に把握できる。
1// NG: 似た構造なのにレイアウトがバラバラ2const name = z.string().min(1).max(100);3const slug = z4 .string()5 .regex(/^[a-z0-9-]+$/)6 .min(1);7const status = z.enum(['active', 'suspended', 'deleted']);89// OK: 同じパターンで揃える10const name = z.string().min(1).max(100);11const slug = z.string().min(1).regex(/^[a-z0-9-]+$/);12const status = z.enum(['active', 'suspended', 'deleted']);
関連する処理をブロックにまとめる
空行で意味の区切りを作る。1 つの空行 = 1 つの段落。
1// OK: 論理的なまとまりごとに空行2export function makeCreateWorkspaceUseCase(ports: Ports) {3 return async (cmd: CreateWorkspaceCommand) => {4 // 1. 入力の検証5 const parsed = CreateWorkspaceSchema.parse(cmd);67 // 2. ビジネスルールの確認8 const existing = await ports.workspaceRepo.findBySlug(parsed.hubId, parsed.slug);9 if (existing) throw new DomainError('WORKSPACE_SLUG_ALREADY_EXISTS');1011 // 3. 集約の構築と保存12 const workspace = buildWorkspace(parsed);13 await ports.workspaceRepo.save(workspace);1415 // 4. Domain Event の発行16 await ports.outbox.publish({17 kind: 'WorkspaceCreated',18 workspaceId: workspace.id,19 });2021 return workspace.id;22 };23}
宣言の順序を意味のある順番にする
対応する HTML / API レスポンスの順序に揃える
重要度の高いフィールドを上に
アルファベット順は最後の手段(意味的な順序がない場合のみ)
5. コメントすべきこと(Ch.5)
コメントの目的 = コードから読み取れないことを伝える
1// NG: コードを言い換えただけ(価値がない)2// account を取得する3const account = await accountRepo.findById(accountId);45// NG: コードのほうが正確(コメントが嘘になるリスク)6// account の名前を返す7return account.displayName; // 実際は displayName を返している
コメントすべき 5 つのケース
1. なぜそうしたか(Why)
1// Instagram API は rate limit が 200 req/hour と厳しいため、2// 一括取得ではなくバッチ化して 50 件ずつ処理する3const BATCH_SIZE = 50;4
2. コードの欠陥(TODO / HACK / FIXME)
1// TODO(GHF-000): hub_delegation の resolvedBy は Phase 2 で実装2// HACK: drizzle-dbml-generator が composite FK の名前を正しく出力しないため手動調整3// FIXME: タイムゾーンの扱いが JST 前提になっている
プロジェクトでは TODO に Issue 番号を付ける: TODO(GHF-000):
3. 定数の背景
1// Stripe の webhook は最大 5 分のリトライ遅延があるため、2// 重複排除ウィンドウは 10 分に設定3const IDEMPOTENCY_WINDOW_MS = 10 * 60 * 1000;
4. 読み手への注意喚起
1// 注意: この関数はトランザクション外で呼ぶこと。2// トランザクション内で呼ぶと FOR UPDATE SKIP LOCKED がデッドロックの原因になる。3export async function pollOutbox(db: DrizzleClient): Promise<OutboxEvent[]> { ... }
5. 全体像・要約(ファイルの冒頭)
TSDoc の @remarks を使って、ファイルやモジュールの全体像を書く。
1/**2 * Outbox ポーラー。3 *4 * @remarks5 * `sd_main.sdapp_outbox_event` から pending イベントを取得し、6 * BullMQ にディスパッチする。FOR UPDATE SKIP LOCKED を使い、7 * 複数 Worker が並行動作しても安全。8 *9 * @see ADR-2026-027 — Transactional Outbox10 */
コメントしなくてよいこと
コードを読めばわかること: 関数名や型が十分に説明的なら不要
型で表現できていること: TypeScript の型が自明なら型の説明を繰り返さない
変更履歴: git log に任せる
この型で表現ができていること。については、TypeScriptの機能を正しく使う観点と、型駆動開発の考え方も併用することが推奨されます。
6. コメントは正確で簡潔に(Ch.6)
代名詞を避ける
1// NG: 「それ」が何を指すか曖昧2// データを取得して、それをキャッシュに入れる3// ↑ 「それ」= 取得結果?加工後のデータ?45// OK: 具体的に6// API レスポンスを取得し、metric 値だけを Redis にキャッシュする
入出力の例を書く
複雑なユーティリティ関数には具体例が最もわかりやすい。
12/**3 * slug をケバブケースに正規化する。4 *5 * @example6 * ```ts7 * normalizeSlug('My Workspace!!') // => 'my-workspace'8 * normalizeSlug('日本語 テスト') // => ''(非 ASCII は除去)9 * ```10 */11export function normalizeSlug(input: string): string { ... }12
意図を正確に伝える
1// NG: 曖昧2// リストの末尾の要素を返す34// OK: 境界条件を含めて正確5// リストの末尾の要素を返す。空の場合は undefined を返す。
7. 制御フローを読みやすく(Ch.7)
条件の書き方: 左に「調べる値」、右に「比較対象」
1// OK: 自然に読める — 「length が 10 より大きいか?」2if (items.length > 10) { ... }34// NG: ヨーダ記法(不自然)5if (10 < items.length) { ... }
早期 return(ガード節)
ネストを減らし、例外ケースを先に処理する。
1// NG: ネストが深い2async function getWorkspace(scope: WorkspaceScope) {3 const workspace = await repo.findById(scope.workspaceId);4 if (workspace) {5 if (workspace.status === 'active') {6 return workspace;7 } else {8 throw new DomainError('WORKSPACE_NOT_ACTIVE');9 }10 } else {11 throw new DomainError('RESOURCE_NOT_FOUND');12 }13}1415// OK: ガード節で早期 return16async function getWorkspace(scope: WorkspaceScope) {17 const workspace = await repo.findById(scope.workspaceId);18 if (!workspace) throw new DomainError('RESOURCE_NOT_FOUND');19 if (workspace.status !== 'active') throw new DomainError('WORKSPACE_NOT_ACTIVE');2021 return workspace;22}
三項演算子は 1 行に収まるときだけ
1// OK: シンプルな二択2const label = isAdmin ? 'Admin' : 'Member';34// NG: 三項演算子のネスト5const label = isAdmin ? 'Admin' : isModerator ? 'Moderator' : 'Member';67// OK: 関数に切り出す8const label = resolveRoleLabel(role);
ネストを減らすテクニック
1. 早期 return(上記)
2. ループ内の continue: 条件を反転してスキップ
1// NG: ネストが深い2for (const event of events) {3 if (event.status === 'pending') {4 if (event.availableAt <= now) {5 await dispatch(event);6 }7 }8}910// OK: continue で条件を反転(※ SD では宣言的スタイルを推奨)11for (const event of events) {12 if (event.status !== 'pending') continue;13 if (event.availableAt > now) continue;14 await dispatch(event);15}1617// Best(弊社プロジェクト 推奨): 宣言的に18const dispatchable = events.filter(19 (e) => e.status === 'pending' && e.availableAt <= now,20);21await Promise.all(dispatchable.map(dispatch));
8. 巨大な式を分割する(Ch.8)
説明変数(Explaining Variable)
複雑な式の途中結果に名前を付けると、読みやすさが劇的に上がる。
1// NG: 1 行に詰め込みすぎ2if (account.emailVerifiedAt && account.status === 'active' && !account.suspendedAt && workspace.status === 'active') {3 // ...4}56// OK: 意図が名前でわかる7const isAccountReady = account.emailVerifiedAt != null8 && account.status === 'active'9 && account.suspendedAt == null;10const isWorkspaceOperable = workspace.status === 'active';1112if (isAccountReady && isWorkspaceOperable) {13 // ...14}
ド・モルガンの法則で否定を簡潔に
1// 読みにくい: 二重否定 + AND2if (!(a && b)) → if (!a || !b)34// 読みにくい: 二重否定 + OR5if (!(a || b)) → if (!a && !b)67// NG8if (!(workspace.status !== 'deleted' && account.status !== 'suspended')) { ... }910// OK: ド・モルガンで変換11if (workspace.status === 'deleted' || account.status === 'suspended') { ... }
短絡評価の乱用を避ける
1// NG: 短絡評価で副作用(何をしているか追いにくい)2isAdmin && deleteWorkspace(id);34// OK: 明示的な if5if (isAdmin) {6 deleteWorkspace(id);7}8
9. 変数と読みやすさ(Ch.9)
不要な変数を削除する
1// NG: 1 回しか使わない中間変数2const now = new Date();3const isExpired = token.expiresAt < now;4if (isExpired) { ... }56// OK(isExpired が意味を追加しない場合)7if (token.expiresAt < new Date()) { ... }89// ただし: 変数名が意味を追加するなら残す(§8 の説明変数)10const isTokenExpired = token.expiresAt < new Date();11if (isTokenExpired) { ... }
判断基準: 変数名が読み手の理解を助けるなら残す。単に式を移動しただけなら削除する。
変数のスコープを縮める
変数は使う場所にできるだけ近くで宣言する。
12// NG: 50 行先で使う変数を先頭で宣言3const hub = await hubRepo.findById(hubId);4// ... 50 行の別の処理 ...5const workspace = buildWorkspace({ hubId: hub.id, ... });67// OK: 使う直前で宣言8// ... 50 行の別の処理 ...9const hub = await hubRepo.findById(hubId);10const workspace = buildWorkspace({ hubId: hub.id, ... });
変数への書き込みは 1 回だけ(const)
弊社プロジェクトでは let を原則禁止しているが、その根拠がここにある。変数が変更されないなら、読み手は「この変数はここで見た値のままだ」と安心して読み進められる。
10. 無関係な下位問題を抽出する(Ch.10)
関数の中で「本来の目的と無関係な処理」を見つけたら、ユーティリティ関数に抽出する。
抽出の判断基準
質問: 「この関数の高レベルの目的は何か?」
→ その目的に直接関係ない行があれば抽出候補。
1// NG: UseCase の中に URL エンコードの詳細がある2async function createInviteLink(scope: WorkspaceScope): Promise<string> {3 const token = generateToken();4 await tokenRepo.save(token);56 // ↓ これは UseCase の目的と無関係な下位問題7 const encodedWorkspaceId = encodeURIComponent(scope.workspaceId);8 const encodedToken = encodeURIComponent(token.value);9 const url = `${origin}/invite?workspace=${encodedWorkspaceId}&token=${encodedToken}`;1011 return url;12}1314// OK: 下位問題を抽出15async function createInviteLink(scope: WorkspaceScope): Promise<string> {16 const token = generateToken();17 await tokenRepo.save(token);1819 return buildInviteUrl(origin, scope.workspaceId, token.value);20}2122function buildInviteUrl(origin: string, workspaceId: string, token: string): string {23 const params = new URLSearchParams({ workspace: workspaceId, token });24 return `${origin}/invite?${params}`;25}
汎用コード vs プロジェクト固有コード
種類例配置
完全に汎用
文字列操作、日付フォーマット
言語/ライブラリの機能を使う
プロジェクト汎用
Outbox イベントの構築
packages/domain のヘルパー
機能固有
Invite URL の構築
同じファイル内の private 関数
11. 一度に一つのタスク(Ch.11)
1 つの関数が複数の「タスク」を同時にこなしていたら、タスクごとに分割する。
タスクの見つけ方
関数の中で「ここから別のことをしている」と感じる行を探す。
12// NG: 1 つの関数に 3 つのタスクが混在3async function handleWebhook(rawBody: string, signature: string) {4 // タスク 1: 署名の検証5 const isValid = verifySignature(rawBody, signature, secret);6 if (!isValid) throw new Error('Invalid signature');78 // タスク 2: ペイロードのパース + 変換9 const payload = JSON.parse(rawBody);10 const event = {11 type: payload.object === 'instagram_business_account' ? 'instagram' : 'unknown',12 accountId: payload.entry?.[0]?.id ?? '',13 timestamp: new Date(payload.time * 1000),14 };1516 // タスク 3: 保存17 await eventRepo.save(event);18}1920// OK: タスクごとに分割21async function handleWebhook(rawBody: string, signature: string) {22 assertValidSignature(rawBody, signature, secret);23 const event = parseInstagramWebhookPayload(rawBody);24 await eventRepo.save(event);25}26
12. 思考をコードに変換する(Ch.12)
「ラバーダッキング」: 処理を自然言語で説明してからコードにする
コードを書く前に、自然言語で「何をしたいか」を説明してみる。その説明がそのままコードの構造になる。
1自然言語:2 「ユーザーがワークスペースのオーナーかどうかを確認する。3 オーナーでなければエラー。4 オーナーならワークスペースを削除状態に変更して保存する。5 削除イベントを Outbox に書く。」
↓ そのままコードにする
1async function deleteWorkspace(scope: WorkspaceScope) {2 if (scope.role !== 'owner') throw new DomainError('INSUFFICIENT_ROLE');34 const workspace = await workspaceRepo.findById(scope.workspaceId);5 if (!workspace) throw new DomainError('RESOURCE_NOT_FOUND');67 const deleted = markAsDeleted(workspace);8 await workspaceRepo.save(deleted);9 await outbox.publish({ kind: 'WorkspaceDeleted', workspaceId: deleted.id });10}11
自然言語で説明したときに「わかりにくい」なら、コードもわかりにくくなっている。
13. 短いコードを書く(Ch.13)
最も読みやすいコードは、書かれていないコード
ライブラリに任せる: URL パース、日付計算、暗号化を自前で実装しない
要件を疑う: 本当に必要な機能だけ実装する(YAGNI と同じ精神)
コードに慣れ親しむ: 標準ライブラリや Drizzle / Zod / Hono の API を知っていれば、車輪の再発明を避けられる
1// NG: Date の差分を自前計算2const diffMs = date2.getTime() - date1.getTime();3const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));45// OK: ライブラリに任せる(例: date-fns)6import { differenceInDays } from 'date-fns';7const diffDays = differenceInDays(date2, date1);89// NG: クエリパラメータを手動で組み立てる10const url = `${base}?workspace=${encodeURIComponent(wsId)}&token=${encodeURIComponent(token)}`;1112// OK: 標準 API を使う13const url = new URL('/invite', base);14url.searchParams.set('workspace', wsId);15url.searchParams.set('token', token);
14. テストと読みやすさ(Ch.14)
テストコードも「読みやすさ」が重要
テストは仕様の文書でもある。テストが読みにくければ、仕様が伝わらず、メンテナンスもされなくなる。
テストの構造: Arrange / Act / Assert
1test('suspended な workspace では投稿できない', () => {2 // Arrange: テストの前提条件3 const workspace: Workspace = {4 kind: 'suspended',5 id: 'ws_001' as WorkspaceId,6 name: 'Test',7 suspendedAt: new Date(),8 };910 // Act & Assert: 実行と検証11 expect(() => assertWorkspaceOperable(workspace)).toThrow('WORKSPACE_NOT_ACTIVE');12});
テストのエラーメッセージを改善する
1// NG: 失敗時に何がどう違うかわからない2expect(result).toBe(true);34// OK: カスタムメッセージで文脈がわかる5expect(result).toBe(true);6// さらに良い: 具体的な値を比較する7expect(workspace.status).toBe('active');
テスト用のヘルパー関数を作る
テストコードの重複を減らし、テストの意図を明確にする。
1// OK: fixture factory で前提条件を簡潔に2function buildActiveWorkspace(overrides?: Partial<Workspace>): Workspace {3 return {4 kind: 'active',5 id: 'ws_test' as WorkspaceId,6 name: 'Test Workspace',7 ...overrides,8 };9}1011test('active な workspace は操作可能', () => {12 const workspace = buildActiveWorkspace();13 expect(() => assertWorkspaceOperable(workspace)).not.toThrow();14});1516test('名前が変更できる', () => {17 const workspace = buildActiveWorkspace({ name: 'Old Name' });18 const renamed = renameWorkspace(workspace, 'New Name');19 expect(renamed.name).toBe('New Name');20});
Từ marketing và phân phối tại châu Á đến thiết kế và phát triển IT, 5SENSE là đối tác chiến lược của bạn. Hãy liên hệ với chúng tôi.
Business Outline
- View Detail
International Business Strategy Support Service
Bộ phận IBS
Nâng cao "độ phân giải thị trường" và thiết kế kịch bản gia nhập chiến thắng. Chúng tôi nắm bắt lối sống và tập quán kinh doanh tại ASEAN qua trải nghiệm thực tế, làm sáng tỏ cấu trúc thị trường thật mà nghiên cứu bàn giấy không thể thấy. Kết hợp dữ liệu với hiểu biết bản địa, chúng tôi xây dựng chiến lược gắn trực tiếp với quyết định.
- View Detail
Sales & Marketing Support Service
Bộ phận Bán hàng & Tiếp thị
Xóa bỏ "sự đứt gãy" và kết nối chiến lược trực tiếp với kết quả mà vẫn giữ nguyên độ thuần. Để giải quyết sự đứt gãy giữa "chiến lược và thực thi" hay "kỹ thuật số và ngoại tuyến," PMO của chúng tôi quản lý xuyên suốt từ hoạch định chiến lược đến triển khai thực địa.
- View Detail
IT Development & Digital Transformation
Phát triển IT & hỗ trợ
Tận dụng công nghệ để "hệ thống hóa" và "mở rộng" nền tảng kinh doanh. Dựa trên thế mạnh phát triển full-scratch — định nghĩa yêu cầu từ các thách thức tiềm ẩn — chúng tôi cung cấp hạ tầng mở rộng tăng trưởng kinh doanh từ góc độ công nghệ.
More Info
Contact us
Hãy liên hệ với chúng tôi về dự án mới, tài liệu giới thiệu hoặc mọi câu hỏi khác.

