Creator Blog

Chi tiết Creator Blog

自動生成ヒーロー(OGスタイル): リーダブルコードを読んでみてのあらためての要約ガイド

リーダブルコードを読んでみてのあらためての要約ガイド

TypeScript

はじめに

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);
3
4// 長いが理解しやすい
5const activeItemsTotal = items
6 .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// NG
2function getPage(url: string): Promise<string> { ... }
3
4// OK: 「HTTP で取得する」ことが名前からわかる
5function fetchPageContent(url: string): Promise<string> { ... }

具体的な名前 > 抽象的な名前

1// NG: run で何を走らせるのかわからない
2function run(): void { ... }
3
4// OK: 何をするか具体的
5function pollOutboxAndDispatchJobs(): void { ... }


名前に追加情報を付ける

単位・状態・形式が重要なら名前に含める。

1// NG: 単位がわからない
2const timeout = 300;
3const size = 1024;
4
5// OK: 単位が名前からわかる
6const timeoutMs = 300;
7const fileSizeBytes = 1024;
8
9// NG: エンコード済みかどうかわからない
10const url = userInput;
11
12// OK: 状態が名前から明確
13const rawUrl = userInput;
14const sanitizedUrl = encodeURI(rawUrl);
15

名前の長さはスコープに比例させる

1// OK: 狭いスコープでは短い名前
2const ids = workspaces.map((ws) => ws.id);
3
4// 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 = 有効?無効?
3
4// OK: 肯定形
5const authEnabled = true;
6
7// NG: 名前の意味が曖昧
8const valid = checkWorkspace(ws);
9
10// OK: true が何を意味するか明確
11const isWorkspaceOperable = ws.status === 'active';
12

filter / select / exclude の使い分け

filter だけでは「含む」「除外する」どちらか曖昧になりやすい。

1// 曖昧: deleted を含む?除外する?
2const filtered = filterByStatus(workspaces, 'deleted');
3
4
5// 明確
6const withoutDeleted = excludeDeletedWorkspaces(workspaces);
7const activeOnly = selectActiveWorkspaces(workspaces);



4. 美しさ(Ch.4)

一貫したレイアウトパターン

読み手が「パターン」を認識できれば、構造を瞬時に把握できる。

1// NG: 似た構造なのにレイアウトがバラバラ
2const name = z.string().min(1).max(100);
3const slug = z
4 .string()
5 .regex(/^[a-z0-9-]+$/)
6 .min(1);
7const status = z.enum(['active', 'suspended', 'deleted']);
8
9// 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);
6
7 // 2. ビジネスルールの確認
8 const existing = await ports.workspaceRepo.findBySlug(parsed.hubId, parsed.slug);
9 if (existing) throw new DomainError('WORKSPACE_SLUG_ALREADY_EXISTS');
10
11 // 3. 集約の構築と保存
12 const workspace = buildWorkspace(parsed);
13 await ports.workspaceRepo.save(workspace);
14
15 // 4. Domain Event の発行
16 await ports.outbox.publish({
17 kind: 'WorkspaceCreated',
18 workspaceId: workspace.id,
19 });
20
21 return workspace.id;
22 };
23}


宣言の順序を意味のある順番にする

対応する HTML / API レスポンスの順序に揃える

重要度の高いフィールドを上に

アルファベット順は最後の手段(意味的な順序がない場合のみ)


5. コメントすべきこと(Ch.5)

コメントの目的 = コードから読み取れないことを伝える

1// NG: コードを言い換えただけ(価値がない)
2// account を取得する
3const account = await accountRepo.findById(accountId);
4
5// 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 * @remarks
5 * `sd_main.sdapp_outbox_event` から pending イベントを取得し、
6 * BullMQ にディスパッチする。FOR UPDATE SKIP LOCKED を使い、
7 * 複数 Worker が並行動作しても安全。
8 *
9 * @see ADR-2026-027 — Transactional Outbox
10 */


コメントしなくてよいこと

コードを読めばわかること: 関数名や型が十分に説明的なら不要

型で表現できていること: TypeScript の型が自明なら型の説明を繰り返さない

変更履歴: git log に任せる

この型で表現ができていること。については、TypeScriptの機能を正しく使う観点と、型駆動開発の考え方も併用することが推奨されます。


6. コメントは正確で簡潔に(Ch.6)

代名詞を避ける

1// NG: 「それ」が何を指すか曖昧
2// データを取得して、それをキャッシュに入れる
3// ↑ 「それ」= 取得結果?加工後のデータ?
4
5// OK: 具体的に
6// API レスポンスを取得し、metric 値だけを Redis にキャッシュする


入出力の例を書く

複雑なユーティリティ関数には具体例が最もわかりやすい。

1
2/**
3 * slug をケバブケースに正規化する。
4 *
5 * @example
6 * ```ts
7 * normalizeSlug('My Workspace!!') // => 'my-workspace'
8 * normalizeSlug('日本語 テスト') // => ''(非 ASCII は除去)
9 * ```
10 */
11export function normalizeSlug(input: string): string { ... }
12

意図を正確に伝える

1// NG: 曖昧
2// リストの末尾の要素を返す
3
4// OK: 境界条件を含めて正確
5// リストの末尾の要素を返す。空の場合は undefined を返す。



7. 制御フローを読みやすく(Ch.7)

条件の書き方: 左に「調べる値」、右に「比較対象」

1// OK: 自然に読める — 「length が 10 より大きいか?」
2if (items.length > 10) { ... }
3
4// 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}
14
15// OK: ガード節で早期 return
16async 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');
20
21 return workspace;
22}


三項演算子は 1 行に収まるときだけ

1// OK: シンプルな二択
2const label = isAdmin ? 'Admin' : 'Member';
3
4// NG: 三項演算子のネスト
5const label = isAdmin ? 'Admin' : isModerator ? 'Moderator' : 'Member';
6
7// 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}
9
10// 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}
16
17// 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}
5
6// OK: 意図が名前でわかる
7const isAccountReady = account.emailVerifiedAt != null
8 && account.status === 'active'
9 && account.suspendedAt == null;
10const isWorkspaceOperable = workspace.status === 'active';
11
12if (isAccountReady && isWorkspaceOperable) {
13 // ...
14}


ド・モルガンの法則で否定を簡潔に

1// 読みにくい: 二重否定 + AND
2if (!(a && b))if (!a || !b)
3
4// 読みにくい: 二重否定 + OR
5if (!(a || b))if (!a && !b)
6
7// NG
8if (!(workspace.status !== 'deleted' && account.status !== 'suspended')) { ... }
9
10// OK: ド・モルガンで変換
11if (workspace.status === 'deleted' || account.status === 'suspended') { ... }


短絡評価の乱用を避ける

1// NG: 短絡評価で副作用(何をしているか追いにくい)
2isAdmin && deleteWorkspace(id);
3
4// OK: 明示的な if
5if (isAdmin) {
6 deleteWorkspace(id);
7}
8


9. 変数と読みやすさ(Ch.9)

不要な変数を削除する

1// NG: 1 回しか使わない中間変数
2const now = new Date();
3const isExpired = token.expiresAt < now;
4if (isExpired) { ... }
5
6// OK(isExpired が意味を追加しない場合)
7if (token.expiresAt < new Date()) { ... }
8
9// ただし: 変数名が意味を追加するなら残す(§8 の説明変数)
10const isTokenExpired = token.expiresAt < new Date();
11if (isTokenExpired) { ... }


判断基準: 変数名が読み手の理解を助けるなら残す。単に式を移動しただけなら削除する。

変数のスコープを縮める

変数は使う場所にできるだけ近くで宣言する。

1
2// NG: 50 行先で使う変数を先頭で宣言
3const hub = await hubRepo.findById(hubId);
4// ... 50 行の別の処理 ...
5const workspace = buildWorkspace({ hubId: hub.id, ... });
6
7// 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);
5
6 // ↓ これは UseCase の目的と無関係な下位問題
7 const encodedWorkspaceId = encodeURIComponent(scope.workspaceId);
8 const encodedToken = encodeURIComponent(token.value);
9 const url = `${origin}/invite?workspace=${encodedWorkspaceId}&token=${encodedToken}`;
10
11 return url;
12}
13
14// OK: 下位問題を抽出
15async function createInviteLink(scope: WorkspaceScope): Promise<string> {
16 const token = generateToken();
17 await tokenRepo.save(token);
18
19 return buildInviteUrl(origin, scope.workspaceId, token.value);
20}
21
22function 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 つの関数が複数の「タスク」を同時にこなしていたら、タスクごとに分割する。

タスクの見つけ方

関数の中で「ここから別のことをしている」と感じる行を探す。

1
2// 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');
7
8 // タスク 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 };
15
16 // タスク 3: 保存
17 await eventRepo.save(event);
18}
19
20// 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');
3
4 const workspace = await workspaceRepo.findById(scope.workspaceId);
5 if (!workspace) throw new DomainError('RESOURCE_NOT_FOUND');
6
7 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));
4
5// OK: ライブラリに任せる(例: date-fns)
6import { differenceInDays } from 'date-fns';
7const diffDays = differenceInDays(date2, date1);
8
9// NG: クエリパラメータを手動で組み立てる
10const url = `${base}?workspace=${encodeURIComponent(wsId)}&token=${encodeURIComponent(token)}`;
11
12// 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 };
9
10 // Act & Assert: 実行と検証
11 expect(() => assertWorkspaceOperable(workspace)).toThrow('WORKSPACE_NOT_ACTIVE');
12});


テストのエラーメッセージを改善する

1// NG: 失敗時に何がどう違うかわからない
2expect(result).toBe(true);
3
4// 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}
10
11test('active な workspace は操作可能', () => {
12 const workspace = buildActiveWorkspace();
13 expect(() => assertWorkspaceOperable(workspace)).not.toThrow();
14});
15
16test('名前が変更できる', () => {
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.

Contact Us

More Info