- Authors

- Name
- Youngju Kim
- @fjvbn20031
- はじめに
- カテゴリ1:命名(めいめい)(Naming) — 6つのルール
- カテゴリ2:関数(かんすう)(Functions) — 6つのルール
- カテゴリ3:コメント — 4つのルール
- カテゴリ4:エラー処理(しょり) — 4つのルール
- カテゴリ5:コード構造化(こうぞうか) — 5つのルール
- カテゴリ6:テスト — 5つのルール
- コードスメルカタログ
- リファクタリング技法(ぎほう) Before/After
- 認知的(にんちてき)複雑度(ふくざつど)(Cognitive Complexity)
- SOLID原則(げんそく)クイックリファレンス
- 実践(じっせん)クイズ
- ツールと自動化(じどうか)
- 参考(さんこう)資料(しりょう)
はじめに
「動(うご)くコード」と「読(よ)みやすいコード」の間(あいだ)には、越(こ)えられないような隔(へだ)たりがあります。多(おお)くの開発者(かいはつしゃ)は機能(きのう)実装(じっそう)に集中(しゅうちゅう)するあまりコード品質(ひんしつ)を後回(あとまわ)しにし、結局(けっきょく)メンテナンスコストが雪(ゆき)だるま式(しき)に膨(ふく)れ上(あ)がる経験(けいけん)をします。
Robert C. Martin(Uncle Bob)の「Clean Code」が出版(しゅっぱん)されてから17年(ねん)が経(た)ちましたが、その原則(げんそく)は今(いま)でも有効(ゆうこう)です。ただし、TypeScript、Python、Rustなどのモダンな言語(げんご)やツールの登場(とうじょう)により、実践(じっせん)方法(ほうほう)は進化(しんか)しています。
この記事(きじ)では、30の実践(じっせん)ルールを6つのカテゴリに分(わ)けて、それぞれ**Bad/Goodコード比較(ひかく)**で説明(せつめい)します。すべての例(れい)はTypeScriptとPythonで提供(ていきょう)されます。
ボーイスカウトルール(Boy Scout Rule): コードを見(み)つけた時(とき)よりもきれいな状態(じょうたい)で残(のこ)せ。
カテゴリ1:命名(めいめい)(Naming) — 6つのルール
ルール1:意図(いと)を伝(つた)える名前(なまえ)を使(つか)え
変数(へんすう)、関数(かんすう)、クラスの名前(なまえ)だけで「何(なに)をするか」「なぜ存在(そんざい)するか」が分(わ)かるべきです。
Bad:
const d: number = 86400;
const list1: number[] = [1, 2, 3];
function calc(a: number, b: number): number {
return a * b * 0.1;
}
Good:
const SECONDS_PER_DAY: number = 86400;
const activeUserIds: number[] = [1, 2, 3];
function calculateDiscountedPrice(originalPrice: number, quantity: number): number {
const DISCOUNT_RATE = 0.1;
return originalPrice * quantity * DISCOUNT_RATE;
}
Python版(ばん):
# Bad
d = 86400
lst = [1, 2, 3]
def calc(a, b):
return a * b * 0.1
# Good
SECONDS_PER_DAY = 86400
active_user_ids = [1, 2, 3]
def calculate_discounted_price(original_price: float, quantity: int) -> float:
DISCOUNT_RATE = 0.1
return original_price * quantity * DISCOUNT_RATE
ルール2:略語(りゃくご)を避(さ)けよ
略語(りゃくご)は作成者(さくせいしゃ)にのみ明確(めいかく)で、他(ほか)の開発者(かいはつしゃ)には謎(なぞ)です。
Bad:
const usrMgr = new UserManager();
const btnClkHndlr = () => {};
const genRpt = (dt: Date) => {};
Good:
const userManager = new UserManager();
const handleButtonClick = () => {};
const generateReport = (reportDate: Date) => {};
許容(きょよう)される略語(りゃくご): 業界(ぎょうかい)標準(ひょうじゅん)のみ — id, url, api, db, io, html, css, http
ルール3:検索(けんさく)可能(かのう)な名前(なまえ)を使(つか)え
マジックナンバーや1文字(もじ)変数(へんすう)はプロジェクト全体(ぜんたい)で検索(けんさく)できません。
Bad:
if (user.role === 5) {
setTimeout(callback, 86400000);
}
Good:
const ADMIN_ROLE_ID = 5;
const ONE_DAY_IN_MILLISECONDS = 86400000;
if (user.role === ADMIN_ROLE_ID) {
setTimeout(callback, ONE_DAY_IN_MILLISECONDS);
}
ルール4:発音(はつおん)できる名前(なまえ)を使(つか)え
コードレビューで名前(なまえ)を声(こえ)に出(だ)して読(よ)めるべきです。
Bad:
class DtaRcrd102 {
private genymdhms: Date;
private modymdhms: Date;
private pszqint: number;
}
Good:
class Customer {
private generationTimestamp: Date;
private modificationTimestamp: Date;
private recordId: number;
}
ルール5:一貫(いっかん)した語彙(ごい)を使(つか)え
同(おな)じ概念(がいねん)に複数(ふくすう)の単語(たんご)を混用(こんよう)しないでください。
Bad:
class UserService {
fetchUser(id: number) {}
retrieveAccount(id: number) {}
getUserProfile(id: number) {}
obtainUserSettings(id: number) {}
}
Good:
class UserService {
getUser(id: number) {}
getAccount(id: number) {}
getUserProfile(id: number) {}
getUserSettings(id: number) {}
}
チーム命名(めいめい)規約(きやく)の例(れい):
| アクション | 規約(きやく) | 例(れい) |
|---|---|---|
| 取得(しゅとく)(単件(たんけん)) | get + 名詞(めいし) | getUser, getOrder |
| 取得(しゅとく)(一覧(いちらん)) | list + 名詞(めいし)複数形(ふくすうけい) | listUsers, listOrders |
| 作成(さくせい) | create + 名詞(めいし) | createUser, createOrder |
| 更新(こうしん) | update + 名詞(めいし) | updateUser, updateOrder |
| 削除(さくじょ) | delete + 名詞(めいし) | deleteUser, deleteOrder |
| 存在(そんざい)確認(かくにん) | has/is + 形容詞(けいようし)/名詞(めいし) | hasPermission, isActive |
| 変換(へんかん) | to + 目的形態(もくてきけいたい) | toJSON, toString |
ルール6:スコープの長(なが)さに比例(ひれい)する名前(なまえ)の長(なが)さ
短(みじか)いスコープでは短(みじか)い名前(なまえ)を、広(ひろ)いスコープでは説明的(せつめいてき)な名前(なまえ)を使(つか)います。
Bad:
for (let arrayIndex = 0; arrayIndex < items.length; arrayIndex++) {
process(items[arrayIndex]);
}
const T = 3000;
Good:
for (let i = 0; i < items.length; i++) {
process(items[i]);
}
const API_REQUEST_TIMEOUT_MS = 3000;
カテゴリ2:関数(かんすう)(Functions) — 6つのルール
ルール7:関数(かんすう)は小(ちい)さく作(つく)れ
関数(かんすう)の理想的(りそうてき)な長(なが)さは20行(ぎょう)以内(いない)です。
Bad:
function processOrder(order: Order): OrderResult {
// 検証 (20行)
if (!order.items || order.items.length === 0) throw new Error('No items');
if (!order.customer) throw new Error('No customer');
// 価格計算 (30行)
let total = 0;
for (const item of order.items) {
total += item.price * item.quantity;
}
const tax = total * 0.1;
const shipping = total > 100 ? 0 : 10;
// 決済処理 (20行)
const paymentResult = chargeCard(order.customer.cardId, total + tax + shipping);
// 通知送信 (15行)
sendEmail(order.customer.email, 'Order confirmed');
return { orderId: generateId(), total, status: 'completed' };
}
Good:
function processOrder(order: Order): OrderResult {
validateOrder(order);
const pricing = calculatePricing(order.items);
const paymentResult = processPayment(order.customer, pricing.total);
notifyCustomer(order.customer, paymentResult);
return createOrderResult(paymentResult, pricing);
}
function validateOrder(order: Order): void {
if (!order.items?.length) throw new InvalidOrderError('No items');
if (!order.customer) throw new InvalidOrderError('No customer');
}
function calculatePricing(items: OrderItem[]): Pricing {
const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
const tax = subtotal * TAX_RATE;
const shipping = subtotal > FREE_SHIPPING_THRESHOLD ? 0 : SHIPPING_FEE;
return { subtotal, tax, shipping, total: subtotal + tax + shipping };
}
ルール8:一(ひと)つのことだけをせよ(Single Responsibility)
関数(かんすう)は一(ひと)つのことだけをすべきで、それをうまくやるべきです。
Bad:
function emailClients(clients: Client[]): void {
for (const client of clients) {
const record = database.lookup(client.id);
if (record.isActive()) {
email(client.email, 'Welcome!');
}
}
}
Good:
function emailActiveClients(clients: Client[]): void {
const activeClients = getActiveClients(clients);
activeClients.forEach(client => sendWelcomeEmail(client));
}
function getActiveClients(clients: Client[]): Client[] {
return clients.filter(client => isClientActive(client.id));
}
function isClientActive(clientId: number): boolean {
return database.lookup(clientId).isActive();
}
function sendWelcomeEmail(client: Client): void {
email(client.email, 'Welcome!');
}
ルール9:パラメータは少(すく)なく保(たも)て(理想(りそう)は2つ以下(いか))
パラメータが3つ以上(いじょう)の場合(ばあい)はオブジェクトにまとめて渡(わた)しましょう。
Bad:
function createUser(
name: string, email: string, age: number,
role: string, department: string, isActive: boolean
): User { /* ... */ }
createUser('Alice', 'alice@example.com', 30, 'admin', 'engineering', true);
Good:
interface CreateUserParams {
name: string;
email: string;
age: number;
role: UserRole;
department: string;
isActive: boolean;
}
function createUser(params: CreateUserParams): User { /* ... */ }
createUser({
name: 'Alice',
email: 'alice@example.com',
age: 30,
role: UserRole.ADMIN,
department: 'engineering',
isActive: true,
});
ルール10:副作用(ふくさよう)を避(さ)けよ
関数(かんすう)が予想外(よそうがい)の動作(どうさ)をしてはいけません。
Bad:
let currentUser: User | null = null;
function checkPassword(userName: string, password: string): boolean {
const user = database.findUser(userName);
if (user && user.password === hash(password)) {
// 副作用!パスワード確認関数がセッションを初期化する
currentUser = user;
initializeSession(user);
return true;
}
return false;
}
Good:
function verifyPassword(userName: string, password: string): boolean {
const user = database.findUser(userName);
return user !== null && user.password === hash(password);
}
function login(userName: string, password: string): LoginResult {
if (!verifyPassword(userName, password)) {
return { success: false };
}
const user = database.findUser(userName);
const session = initializeSession(user);
return { success: true, user, session };
}
ルール11:コマンド・クエリ分離(ぶんり)(Command-Query Separation)
関数(かんすう)は「何(なに)かを実行(じっこう)する」か「何(なに)かを返(かえ)す」かのどちらかで、両方(りょうほう)してはいけません。
Bad:
function setAndCheckAttribute(name: string, value: string): boolean {
const attribute = findAttribute(name);
if (attribute) {
attribute.value = value;
return true;
}
return false;
}
if (setAndCheckAttribute('username', 'alice')) { /* ??? */ }
Good:
function attributeExists(name: string): boolean {
return findAttribute(name) !== null;
}
function setAttribute(name: string, value: string): void {
const attribute = findAttribute(name);
if (!attribute) throw new AttributeNotFoundError(name);
attribute.value = value;
}
if (attributeExists('username')) {
setAttribute('username', 'alice');
}
ルール12:DRY(Don't Repeat Yourself)
重複(じゅうふく)コードはバグの温床(おんしょう)です。
Bad:
function getAdminUsers(): User[] {
const users = database.query('SELECT * FROM users');
return users
.filter(u => u.role === 'admin')
.map(u => ({ ...u, displayName: `${u.firstName} ${u.lastName}` }))
.sort((a, b) => a.displayName.localeCompare(b.displayName));
}
function getModeratorUsers(): User[] {
const users = database.query('SELECT * FROM users');
return users
.filter(u => u.role === 'moderator')
.map(u => ({ ...u, displayName: `${u.firstName} ${u.lastName}` }))
.sort((a, b) => a.displayName.localeCompare(b.displayName));
}
Good:
function getUsersByRole(role: UserRole): User[] {
const users = database.query('SELECT * FROM users WHERE role = ?', [role]);
return users.map(addDisplayName).sort(byDisplayName);
}
function addDisplayName(user: User): User {
return { ...user, displayName: `${user.firstName} ${user.lastName}` };
}
function byDisplayName(a: User, b: User): number {
return a.displayName.localeCompare(b.displayName);
}
カテゴリ3:コメント — 4つのルール
ルール13:「なぜ」を説明(せつめい)し、「何(なに)」はコードに任(まか)せよ
Bad:
// ユーザー名を取得する
const userName = user.getName();
// 配列を反復する
for (const item of items) {
// アイテムを処理する
processItem(item);
}
Good:
// 外部API制限により最大100件までリクエスト可能 (API docs: https://...)
const MAX_BATCH_SIZE = 100;
// レガシーシステム互換のためISO-8859-1でエンコードが必要
// 新システム移行後UTF-8に変更予定 (JIRA-4521)
const encodedData = encode(data, 'ISO-8859-1');
ルール14:ノイズコメントを避(さ)けよ
情報(じょうほう)を追加(ついか)しないコメントはコードを読(よ)みにくくします。
Bad:
/**
* コンストラクタ
*/
constructor() {}
/**
* idを返す
* @returns id
*/
getId(): number {
return this.id;
}
Good:
class User {
constructor(
private readonly id: number,
private readonly name: string,
) {}
getId(): number {
return this.id;
}
}
ルール15:TODOコメントは標準(ひょうじゅん)形式(けいしき)に従(したが)え
Bad:
// TODO あとで直す
// FIXME これ動かない
// HACK 一時的にこうした
Good:
// TODO(team-auth): OAuth2トークン更新ロジック追加 [JIRA-1234] @2025-Q2
// FIXME(alice): 同時リクエスト時にrace condition発生。分散ロック導入必要 [JIRA-5678]
// HACK: Safari 17.2 flexboxバグワークアラウンド。Safari 18修正後に削除
ルール16:JSDoc/Docstringを活用(かつよう)せよ
TypeScript (JSDoc):
/**
* 指定期間のユーザー活動統計を計算します。
*
* @param userId - 統計を照会するユーザーID
* @param startDate - 照会開始日(含む)
* @param endDate - 照会終了日(含む)
* @returns 活動統計オブジェクト
* @throws {UserNotFoundError} ユーザーが存在しない場合
* @throws {InvalidDateRangeError} 開始日が終了日より後の場合
*
* @example
* const stats = await getUserActivityStats('user-123', '2025-01-01', '2025-03-31');
* console.log(stats.totalLogins); // 45
*/
async function getUserActivityStats(
userId: string,
startDate: string,
endDate: string,
): Promise<ActivityStats> {
// ...
}
カテゴリ4:エラー処理(しょり) — 4つのルール
ルール17:エラーコードの代(か)わりに例外(れいがい)を使(つか)え
Bad:
function withdraw(account: Account, amount: number): number {
if (amount > account.balance) return -1;
if (amount < 0) return -2;
if (account.isFrozen) return -3;
account.balance -= amount;
return 0;
}
Good:
class InsufficientFundsError extends Error {
constructor(public readonly balance: number, public readonly amount: number) {
super(`Insufficient funds: balance ${balance}, requested ${amount}`);
this.name = 'InsufficientFundsError';
}
}
function withdraw(account: Account, amount: number): void {
if (amount < 0) throw new InvalidAmountError(amount);
if (account.isFrozen) throw new AccountFrozenError(account.id);
if (amount > account.balance) throw new InsufficientFundsError(account.balance, amount);
account.balance -= amount;
}
ルール18:早(はや)く失敗(しっぱい)せよ(Fail Fast)
不正(ふせい)な状態(じょうたい)をできるだけ早(はや)く検出(けんしゅつ)し、即座(そくざ)に報告(ほうこく)すべきです。
Bad:
function processPayment(orderId: string, amount: number): PaymentResult {
// ... 100行のロジック ...
if (!orderId) return { success: false, error: 'Invalid order ID' };
if (amount < 0) return { success: false, error: 'Invalid amount' };
}
Good:
function processPayment(orderId: string, amount: number): PaymentResult {
// ガード節:最初に検証
if (!orderId) throw new InvalidArgumentError('orderId is required');
if (amount <= 0) throw new InvalidArgumentError('amount must be positive');
const order = orderRepository.findById(orderId);
if (!order) throw new OrderNotFoundError(orderId);
return paymentGateway.charge(order, amount);
}
ルール19:カスタムエラーを定義(ていぎ)せよ
abstract class AppError extends Error {
abstract readonly statusCode: number;
abstract readonly isOperational: boolean;
constructor(message: string, public readonly cause?: Error) {
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
class NotFoundError extends AppError {
readonly statusCode = 404;
readonly isOperational = true;
}
class ValidationError extends AppError {
readonly statusCode = 400;
readonly isOperational = true;
constructor(message: string, public readonly field: string, public readonly value: unknown) {
super(message);
}
}
class DatabaseError extends AppError {
readonly statusCode = 500;
readonly isOperational = false; // システムエラー — 運用チームに通知
}
ルール20:汎用(はんよう)例外(れいがい)をcatchするな
Bad:
try {
const data = await fetchUserData(userId);
await processData(data);
await saveToDatabase(data);
} catch (error) {
console.log('Something went wrong'); // すべて飲み込む
}
Good:
try {
const data = await fetchUserData(userId);
await processData(data);
await saveToDatabase(data);
} catch (error) {
if (error instanceof NetworkError) {
logger.warn('Network error', { userId, error });
await retryWithBackoff(() => fetchUserData(userId));
} else if (error instanceof ValidationError) {
logger.error('Invalid user data', { userId, field: error.field });
throw error;
} else {
throw error; // 予期しないエラーは再スロー
}
}
カテゴリ5:コード構造化(こうぞうか) — 5つのルール
ルール21:垂直(すいちょく)フォーマットを守(まも)れ
関連(かんれん)するコードは近(ちか)くに、無関係(むかんけい)なコードは空行(くうぎょう)で区切(くぎ)ります。
ルール22:新聞記事(しんぶんきじ)のメタファー
ファイルを上(うえ)から下(した)に読(よ)む時(とき)、抽象度(ちゅうしょうど)が徐々(じょじょ)に下(さ)がるべきです。
// 1. 最初: パブリックインターフェース(見出し)
export class OrderProcessor {
async processOrder(orderId: string): Promise<OrderResult> {
const order = await this.validateAndFetchOrder(orderId);
const pricing = this.calculatePricing(order);
return this.executePayment(order, pricing);
}
// 2. 中間: ビジネスロジック(本文)
private async validateAndFetchOrder(orderId: string): Promise<Order> {
const order = await this.orderRepo.findById(orderId);
if (!order) throw new OrderNotFoundError(orderId);
this.validateOrderState(order);
return order;
}
private calculatePricing(order: Order): Pricing {
const subtotal = this.sumItems(order.items);
const discount = this.applyDiscounts(subtotal, order.coupons);
const tax = this.calculateTax(subtotal - discount);
return { subtotal, discount, tax, total: subtotal - discount + tax };
}
// 3. 最後: 実装の詳細
private validateOrderState(order: Order): void { /* ... */ }
private sumItems(items: OrderItem[]): number { /* ... */ }
private applyDiscounts(amount: number, coupons: Coupon[]): number { /* ... */ }
private calculateTax(amount: number): number { /* ... */ }
}
ルール23:Import整理(せいり)
// 1. Node.js組み込みモジュール
import { readFile } from 'fs/promises';
import path from 'path';
// 2. 外部ライブラリ (node_modules)
import express from 'express';
import { z } from 'zod';
// 3. 内部モジュール — 絶対パス
import { AppConfig } from '@/config';
import { Logger } from '@/utils/logger';
// 4. 内部モジュール — 相対パス
import { UserService } from './services/user';
// 5. 型Import (TypeScript)
import type { User, Order } from './types';
ルール24:ファイルサイズを制限(せいげん)せよ
| 基準(きじゅん) | 推奨(すいしょう) | 警告(けいこく) | 危険(きけん) |
|---|---|---|---|
| ファイル行数(ぎょうすう) | 200行(ぎょう)以下(いか) | 200〜400行(ぎょう) | 400行(ぎょう)以上(いじょう) |
| 関数(かんすう)行数(ぎょうすう) | 20行(ぎょう)以下(いか) | 20〜50行(ぎょう) | 50行(ぎょう)以上(いじょう) |
| クラスメソッド数(すう) | 10個(こ)以下(いか) | 10〜20個(こ) | 20個(こ)以上(いじょう) |
| パラメータ数(すう) | 2個(こ)以下(いか) | 3〜4個(こ) | 5個(こ)以上(いじょう) |
| ネスト深度(しんど) | 2段階(だんかい)以下(いか) | 3段階(だんかい) | 4段階(だんかい)以上(いじょう) |
ルール25:モジュール境界(きょうかい)を明確(めいかく)にせよ
src/
├── modules/
│ ├── user/
│ │ ├── index.ts # パブリックAPIのみexport
│ │ ├── user.service.ts
│ │ ├── user.repository.ts
│ │ └── __tests__/
│ ├── order/
│ │ ├── index.ts
│ │ └── ...
│ └── payment/
│ ├── index.ts
│ └── ...
├── shared/
│ ├── errors/
│ └── utils/
└── infrastructure/
├── database/
└── cache/
// modules/user/index.ts — パブリックAPIのみ公開
export { UserService } from './user.service';
export type { User, CreateUserDTO } from './user.types';
// UserRepositoryはexportしない — 内部実装
カテゴリ6:テスト — 5つのルール
ルール26:FIRST原則(げんそく)
| 原則(げんそく) | 説明(せつめい) |
|---|---|
| Fast | テストは高速(こうそく)に実行(じっこう)される(ミリ秒(びょう)単位(たんい)) |
| Independent | テスト間(かん)に依存(いぞん)関係(かんけい)がない |
| Repeatable | どの環境(かんきょう)でも同(おな)じ結果(けっか) |
| Self-validating | Pass/Failが自動(じどう)で判定(はんてい)される |
| Timely | プロダクションコードの直前(ちょくぜん)/直後(ちょくご)に作成(さくせい) |
ルール27:AAAパターン(Arrange-Act-Assert)
describe('UserService', () => {
describe('createUser', () => {
it('should create user with valid data', async () => {
// Arrange — テストデータ準備
const userRepo = new InMemoryUserRepository();
const service = new UserService(userRepo);
const userData: CreateUserDTO = {
name: 'Alice',
email: 'alice@example.com',
};
// Act — テスト対象実行
const result = await service.createUser(userData);
// Assert — 結果検証
expect(result.name).toBe('Alice');
expect(result.email).toBe('alice@example.com');
expect(result.id).toBeDefined();
});
});
});
ルール28:1つのテストに1つのAssert概念(がいねん)
Bad:
it('should handle user lifecycle', async () => {
const user = await service.createUser(data);
expect(user.id).toBeDefined();
const fetched = await service.getUser(user.id);
expect(fetched.name).toBe(data.name);
await service.updateUser(user.id, { name: 'Bob' });
const updated = await service.getUser(user.id);
expect(updated.name).toBe('Bob');
await service.deleteUser(user.id);
await expect(service.getUser(user.id)).rejects.toThrow();
});
Good:
describe('UserService', () => {
it('should create user and return with generated id', async () => {
const user = await service.createUser(validUserData);
expect(user.id).toBeDefined();
expect(user.name).toBe(validUserData.name);
});
it('should retrieve existing user by id', async () => {
const created = await service.createUser(validUserData);
const fetched = await service.getUser(created.id);
expect(fetched).toEqual(created);
});
it('should throw NotFoundError when getting deleted user', async () => {
const user = await service.createUser(validUserData);
await service.deleteUser(user.id);
await expect(service.getUser(user.id)).rejects.toThrow(NotFoundError);
});
});
ルール29:読(よ)みやすいテストを書(か)け
Bad:
it('test1', async () => {
const r = await s.calc({ a: 100, b: 2, c: 0.1, d: true });
expect(r).toBe(180);
});
Good:
it('should apply 10% discount when premium member orders 2 items at 100 each', async () => {
// Given
const order = createOrder({
items: [{ price: 100, quantity: 2 }],
isPremiumMember: true,
discountRate: 0.1,
});
// When
const totalPrice = await pricingService.calculateTotal(order);
// Then
expect(totalPrice).toBe(180); // (100 * 2) - 10% discount = 180
});
ルール30:テスト名(めい)はシナリオを説明(せつめい)せよ
命名(めいめい)パターン: should [期待結果] when [条件]
describe('PasswordValidator', () => {
it('should return valid when password has 8+ chars with mixed case and numbers', () => {});
it('should return invalid when password is shorter than 8 characters', () => {});
it('should return invalid when password lacks uppercase letters', () => {});
it('should throw EmptyPasswordError when password is empty string', () => {});
});
コードスメルカタログ
Martin Fowlerのリファクタリングで定義(ていぎ)された代表的(だいひょうてき)なコードスメルとその解決策(かいけつさく)です。
| コードスメル | 症状(しょうじょう) | 解決策(かいけつさく) |
|---|---|---|
| Long Method | 関数(かんすう)が50行(ぎょう)以上(いじょう) | Extract Method |
| Large Class | クラスの責任(せきにん)が3つ以上(いじょう) | Extract Class |
| Primitive Obsession | プリミティブ型(がた)のみでドメインを表現(ひょうげん) | Value Object導入(どうにゅう) |
| Long Parameter List | パラメータ4つ以上(いじょう) | Parameter Object |
| Data Clumps | 複数箇所(ふくすうかしょ)で同(おな)じデータグループ | Extract Class |
| Feature Envy | 他(ほか)のクラスのデータを過度(かど)に使用(しよう) | Move Method |
| Shotgun Surgery | 1つの変更(へんこう)が複数(ふくすう)クラスに影響(えいきょう) | Move Method |
| Dead Code | 使(つか)われていないコード | 削除(さくじょ) |
| Duplicated Code | 同(おな)じコードが2箇所(かしょ)以上(いじょう) | Extract Method |
リファクタリング技法(ぎほう) Before/After
技法(ぎほう)1:ガード節(せつ)でネスト除去(じょきょ)
Before:
function getPaymentAmount(employee: Employee): number {
let result: number;
if (employee.isSeparated) {
result = separatedAmount();
} else {
if (employee.isRetired) {
result = retiredAmount();
} else {
result = normalPayAmount();
}
}
return result;
}
After:
function getPaymentAmount(employee: Employee): number {
if (employee.isSeparated) return separatedAmount();
if (employee.isRetired) return retiredAmount();
return normalPayAmount();
}
技法(ぎほう)2:ストラテジーパターンで条件文(じょうけんぶん)除去(じょきょ)
Before:
function calculateShippingCost(order: Order): number {
if (order.shippingMethod === 'standard') return order.weight * 1.5;
else if (order.shippingMethod === 'express') return order.weight * 3.0 + 5;
else if (order.shippingMethod === 'overnight') return order.weight * 5.0 + 15;
throw new Error('Unknown shipping method');
}
After:
interface ShippingStrategy {
calculate(weight: number): number;
}
const shippingStrategies: Record<string, ShippingStrategy> = {
standard: { calculate: (w) => w * 1.5 },
express: { calculate: (w) => w * 3.0 + 5 },
overnight: { calculate: (w) => w * 5.0 + 15 },
};
function calculateShippingCost(order: Order): number {
const strategy = shippingStrategies[order.shippingMethod];
if (!strategy) throw new UnknownShippingMethodError(order.shippingMethod);
return strategy.calculate(order.weight);
}
技法(ぎほう)3:パイプラインでループリファクタリング
Before:
function getActiveAdminEmails(users: User[]): string[] {
const result: string[] = [];
for (let i = 0; i < users.length; i++) {
if (users[i].isActive) {
if (users[i].role === 'admin') {
result.push(users[i].email.toLowerCase());
}
}
}
result.sort();
return result;
}
After:
function getActiveAdminEmails(users: User[]): string[] {
return users
.filter(user => user.isActive)
.filter(user => user.role === 'admin')
.map(user => user.email.toLowerCase())
.sort();
}
認知的(にんちてき)複雑度(ふくざつど)(Cognitive Complexity)
SonarQubeで測定(そくてい)される認知的(にんちてき)複雑度(ふくざつど)は、コードを「理解(りかい)するために必要(ひつよう)な精神的(せいしんてき)な努力(どりょく)」を数値化(すうちか)します。
推奨(すいしょう)しきい値(ち)
| レベル | 認知的複雑度(にんちてきふくざつど) | アクション |
|---|---|---|
| 良好(りょうこう) | 0〜5 | 維持(いじ) |
| 警告(けいこく) | 6〜10 | リファクタリング検討(けんとう) |
| 危険(きけん) | 11〜15 | 必(かなら)ずリファクタリング |
| 深刻(しんこく) | 16以上(いじょう) | 即座(そくざ)にリファクタリング |
SOLID原則(げんそく)クイックリファレンス
S — 単一責任(たんいつせきにん)の原則(げんそく)
// Bad: 1つのクラスが複数の責任
class UserService {
createUser(data: CreateUserDTO) {}
sendEmail(to: string, body: string) {}
generatePDF(user: User) {}
logAction(action: string) {}
}
// Good: 各クラスが1つの責任
class UserService { createUser(data: CreateUserDTO) {} }
class EmailService { send(to: string, body: string) {} }
class PDFGenerator { generate(user: User) {} }
class AuditLogger { log(action: string) {} }
O — 開放(かいほう)閉鎖(へいさ)の原則(げんそく)
interface PaymentProcessor {
process(amount: number): Promise<PaymentResult>;
}
class StripeProcessor implements PaymentProcessor {
async process(amount: number) { /* Stripe API */ }
}
class PayPalProcessor implements PaymentProcessor {
async process(amount: number) { /* PayPal API */ }
}
// 新しい決済手段の追加は既存コードの修正なしに新クラスを追加するだけ
class CryptoProcessor implements PaymentProcessor {
async process(amount: number) { /* Crypto API */ }
}
L — リスコフの置換(ちかん)原則(げんそく)
// Bad: ペンギンは飛べないのでBirdを継承すべきでない
class Bird { fly(): void { /* 飛ぶ */ } }
class Penguin extends Bird {
fly(): void { throw new Error('Cannot fly!'); }
}
// Good: インターフェースを分離
interface FlyingBird { fly(): void; }
interface SwimmingBird { swim(): void; }
class Eagle implements FlyingBird { fly() {} }
class Penguin implements SwimmingBird { swim() {} }
I — インターフェース分離(ぶんり)の原則(げんそく)
// Bad: 太いインターフェース
interface Worker {
work(): void;
eat(): void;
sleep(): void;
attendMeeting(): void;
}
// Good: 分離されたインターフェース
interface Workable { work(): void; }
interface Feedable { eat(): void; }
interface Meetable { attendMeeting(): void; }
class Developer implements Workable, Feedable, Meetable {
work() {} eat() {} attendMeeting() {}
}
class Robot implements Workable {
work() {}
}
D — 依存性(いぞんせい)逆転(ぎゃくてん)の原則(げんそく)
// Bad: 高レベルモジュールが低レベルモジュールに直接依存
class OrderService {
private mysqlDb = new MySQLDatabase();
save(order: Order) { this.mysqlDb.insert('orders', order); }
}
// Good: 抽象化に依存
interface OrderRepository {
save(order: Order): Promise<void>;
findById(id: string): Promise<Order | null>;
}
class OrderService {
constructor(private readonly repo: OrderRepository) {}
async save(order: Order) { await this.repo.save(order); }
}
class MySQLOrderRepository implements OrderRepository { /* ... */ }
class MongoOrderRepository implements OrderRepository { /* ... */ }
class InMemoryOrderRepository implements OrderRepository { /* ... */ }
実践(じっせん)クイズ
クイズ1:このコードの問題点(もんだいてん)は何(なん)ですか?
function proc(lst: any[], flg: boolean): any[] {
let res: any[] = [];
for (let i = 0; i < lst.length; i++) {
if (flg) {
if (lst[i].a > 0) {
res.push(lst[i].a * 2);
}
} else {
if (lst[i].b !== null) {
res.push(lst[i].b);
}
}
}
return res;
}
問題点(もんだいてん):
- 命名(めいめい): proc, lst, flg, res, a, b すべて意味(いみ)のない名前(なまえ)
- 型(かた): any型(がた)の乱用(らんよう)
- 関数(かんすう)の責任(せきにん): フラグでパラメータの動作(どうさ)が変(か)わるのは2つの関数(かんすう)が必要(ひつよう)というシグナル
- ネスト深度(しんど): 3段階(だんかい)のネスト
- 命令型(めいれいがた)ループ: filter/mapパイプラインで置(お)き換(か)え可能(かのう)
リファクタリング:
function getDoubledPositiveAmounts(transactions: Transaction[]): number[] {
return transactions
.filter(t => t.amount > 0)
.map(t => t.amount * 2);
}
function getNonNullBalances(accounts: Account[]): number[] {
return accounts
.filter(a => a.balance !== null)
.map(a => a.balance);
}
クイズ2:このエラー処理(しょり)コードを改善(かいぜん)してください。
async function getData(id: string) {
try {
const res = await fetch(`/api/data/${id}`);
const data = await res.json();
return data;
} catch (e) {
console.log(e);
return null;
}
}
リファクタリング:
class ApiError extends Error {
constructor(
message: string,
public readonly statusCode: number,
public readonly endpoint: string,
) {
super(message);
this.name = 'ApiError';
}
}
async function fetchDataById(id: string): Promise<UserData> {
const endpoint = `/api/data/${id}`;
const response = await fetch(endpoint);
if (!response.ok) {
throw new ApiError(
`Failed to fetch data: ${response.statusText}`,
response.status,
endpoint,
);
}
return response.json() as Promise<UserData>;
}
クイズ3:このクラスのSOLID違反(いはん)を見(み)つけてください。
class ReportGenerator {
generateReport(data: any[]): string {
if (!data.length) throw new Error('No data');
let html = '<table>';
for (const row of data) {
html += `<tr><td>${row.name}</td><td>${row.value}</td></tr>`;
}
html += '</table>';
fs.writeFileSync('/tmp/report.html', html);
sendEmail('admin@company.com', 'Report', html);
return html;
}
}
違反(いはん)事項(じこう):
- SRP違反(いはん): 検証(けんしょう)、HTML生成(せいせい)、ファイル保存(ほぞん)、メール送信(そうしん)の4つの責任(せきにん)
- OCP違反(いはん): PDF形式(けいしき)の追加(ついか)にはクラスの修正(しゅうせい)が必要(ひつよう)
- DIP違反(いはん): fs、sendEmailに直接(ちょくせつ)依存(いぞん)
リファクタリング:
interface ReportFormatter { format(data: ReportData[]): string; }
interface ReportExporter { export(content: string): Promise<void>; }
class ReportService {
constructor(
private formatter: ReportFormatter,
private exporters: ReportExporter[],
) {}
async generate(data: ReportData[]): Promise<string> {
if (!data.length) throw new EmptyDataError();
const content = this.formatter.format(data);
await Promise.all(this.exporters.map(e => e.export(content)));
return content;
}
}
クイズ4:この関数(かんすう)の認知的(にんちてき)複雑度(ふくざつど)を計算(けいさん)してください。
function calculatePrice(product: Product, user: User): number {
let price = product.basePrice;
if (user.isPremium) { // +1
if (product.category === 'electronics') { // +2
price *= 0.8;
} else { // +1
price *= 0.9;
}
} else { // +1
if (user.hasCoupon) { // +2
price *= 0.95;
}
}
if (product.isOnSale && user.isPremium) { // +1
price *= 0.9;
}
return price;
}
認知的(にんちてき)複雑度(ふくざつど): 8(警告(けいこく)レベル、リファクタリング検討(けんとう)が必要(ひつよう))
リファクタリング:
function calculatePrice(product: Product, user: User): number {
const basePrice = product.basePrice;
const memberDiscount = getMemberDiscount(user, product.category);
const couponDiscount = getCouponDiscount(user);
const saleDiscount = getSaleDiscount(product, user);
return basePrice * memberDiscount * couponDiscount * saleDiscount;
}
function getMemberDiscount(user: User, category: string): number {
if (!user.isPremium) return 1;
return category === 'electronics' ? 0.8 : 0.9;
}
function getCouponDiscount(user: User): number {
return !user.isPremium && user.hasCoupon ? 0.95 : 1;
}
function getSaleDiscount(product: Product, user: User): number {
return product.isOnSale && user.isPremium ? 0.9 : 1;
}
クイズ5:このテストコードを改善(かいぜん)してください。
test('test1', () => {
const s = new CalcService();
expect(s.add(1, 2)).toBe(3);
expect(s.add(-1, 1)).toBe(0);
expect(s.subtract(5, 3)).toBe(2);
expect(s.multiply(3, 4)).toBe(12);
expect(s.divide(10, 2)).toBe(5);
expect(() => s.divide(1, 0)).toThrow();
});
リファクタリング:
describe('CalcService', () => {
const calculator = new CalcService();
describe('add', () => {
it('should return sum of two positive numbers', () => {
expect(calculator.add(1, 2)).toBe(3);
});
it('should handle negative numbers', () => {
expect(calculator.add(-1, 1)).toBe(0);
});
});
describe('divide', () => {
it('should return quotient of two numbers', () => {
expect(calculator.divide(10, 2)).toBe(5);
});
it('should throw DivisionByZeroError when divisor is zero', () => {
expect(() => calculator.divide(1, 0)).toThrow(DivisionByZeroError);
});
});
});
ツールと自動化(じどうか)
ESLint + Prettier設定(せってい)
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"rules": {
"max-lines": ["warn", { "max": 400 }],
"max-lines-per-function": ["warn", { "max": 50 }],
"max-params": ["warn", { "max": 3 }],
"max-depth": ["warn", { "max": 3 }],
"complexity": ["warn", { "max": 10 }],
"no-else-return": "error",
"prefer-const": "error",
"no-var": "error"
}
}
SonarQube Quality Gate
sonar.qualitygate.conditions:
- metric: cognitive_complexity
operator: GT
error: 15
- metric: duplicated_lines_density
operator: GT
error: 3
- metric: coverage
operator: LT
error: 80
参考(さんこう)資料(しりょう)
- Robert C. Martin, "Clean Code: A Handbook of Agile Software Craftsmanship" (2008)
- Martin Fowler, "Refactoring: Improving the Design of Existing Code" (2018, 2nd Edition)
- G. Ann Campbell, "Cognitive Complexity: A new way of measuring understandability" — SonarSource (2023)
- Google Engineering Practices — Code Review Guidelines
- Airbnb JavaScript Style Guide
- TypeScript Deep Dive — Style Guide
- PEP 8 — Python Style Guide
- "The Art of Readable Code" by Dustin Boswell and Trevor Foucher (2011)
- SonarQube Documentation — Cognitive Complexity
- ESLint Rules Reference
- Kent Beck, "Test Driven Development: By Example" (2002)
- Michael Feathers, "Working Effectively with Legacy Code" (2004)
- "A Philosophy of Software Design" by John Ousterhout (2018)
- Clean Code JavaScript — GitHub (ryanmcdermott/clean-code-javascript)