目次
この記事の内容
クリーンコードは、ソフトウェアの長期的な生産性を決定する最も重要な要素の一つです。本記事では、名前付けの原則から関数設計、フォーマット、エラーハンドリングまで、実践的なクリーンコードの書き方を体系的に解説します。
クリーンコードの基本概念
クリーンコードとは
**クリーンコード(Clean Code)**とは、読む人間にとって理解しやすく、変更しやすいコードです。
【クリーンコードの定義】
・一人の著者ではなく、複数人で読むことを前提とする
・一貫性があり、予測可能なパターンに従う
・ビジネスロジックが明確に表現されている
・テストやすく、拡張しやすい
クリーンコードの重要性
【コードが読まれる回数】
Robert C. Martin によれば:
・コードを書く:1 回
・コードを読む:100 回以上(デバッグ、機能追加、リファクタリング)
【悪いコードのコスト】
・理解に時間がかかる
・バグの発見が遅れる
・変更への恐怖が生まれる
・新規機能の追加が困難になる
クリーンコードの 3 原則
| 原則 | 説明 | 具体的な効果 |
|---|---|---|
| 可読性 | 人間が理解しやすい | メンテナンスコスト削減、オンボーディング時間短縮 |
| 単純性 | 必要十分な複雑さ | バグの減少、理解の容易さ |
| 変更のしやすさ | 拡張・修正が容易 | 開発速度の維持、技術的負債の削減 |
意味のある命名
命名の重要性
変数・関数・クラスの名前は、その存在理由、やるべきこと、使い方を物語ります。
【悪い命名の影響】
・コードの意図が伝わらない
・間違った使い方される
・リファクタリング時に見過ごされる
【良い命名の効果】
・コードを読む必要がなくなる(名前自体がドキュメント)
・バグの早期発見
・リファクタリングの容易さ
意図が伝わる名前を使う
// ❌ 悪い例:意味のない缩写
let d; // 何?日数?配列?
let flag; // 何のフラグ?
let data; // どんなデータ?
// ✅ 良い例:意図が明確
let daysSinceLastLogin;
let isUserAuthenticated;
let customerOrderHistory;
// ❌ 悪い例:魔法の数字
if (user.status === 3) {
// 3 とは?
}
// ✅ 良い例:定数を使用
const USER_STATUS_SUSPENDED = 3;
if (user.status === USER_STATUS_SUSPENDED) {
// 停止ユーザーの処理
}
発音できる名前を使う
// ❌ 悪い例:発音できない
let qryUsrRsp; // Query User Response?
// ✅ 良い例:発音できる
let queryUserResponse;
理由:コードレビューやペアプログラミングで「あれ」と呼ぶしかないコードは、コミュニケーションコストを高めます。
検索可能な名前を使う
// ❌ 悪い例:短すぎる
const MAX = 100;
// 検索:'MAX' → 1000 件ヒット(どこで使われているか分からない)
// ✅ 良い例:具体的
const MAX_RETRY_COUNT = 100;
const MAX_USER_NAME_LENGTH = 100;
// 検索:'MAX_RETRY_COUNT' → 5 件ヒット(使用箇所が明確)
列挙型はクラスで表現する
// ❌ 悪い例:魔法の文字列
function getUserType(type) {
if (type === 'prm') {
// premium?
}
}
// ✅ 良い例:定数オブジェクト
const UserType = {
PREMIUM: 'premium',
BASIC: 'basic',
GUEST: 'guest'
};
function getUserType(type) {
if (type === UserType.PREMIUM) {
// 明確な意図
}
}
// ✅ さらに良い例:TypeScript 列挙型
enum UserType {
Premium = 'premium',
Basic = 'basic',
Guest = 'guest'
}
誤解を招く名前を避ける
// ❌ 悪い例:誤解を招く
let accountList; // 実際は Set 型、重複不允许
// ✅ 良い例:正確なデータ構造
let accountSet;
// ❌ 悪い例:連想させない
function getActiveUsers() {
// 実際は全ユーザー(inactive も含む)
}
// ✅ 良い例:実態と一致
function getAllUsers() {
// 全ユーザーを取得
}
関数の設計
関数は小さく
【関数のサイズの目安】
・理想:10 行以内
・許容:20 行以内
・要注意:50 行超(分割を検討)
・危険信号:100 行超(必ず分割)
// ❌ 悪い例:大きすぎる関数
function processOrder(order) {
// 1. 在庫確認(30 行)
let hasStock = true;
for (const item of order.items) {
const product = getProduct(item.productId);
if (product.stock < item.quantity) {
hasStock = false;
break;
}
}
if (!hasStock) {
return { success: false, message: '在庫不足' };
}
// 2. 価格計算(40 行)
let subtotal = 0;
for (const item of order.items) {
const product = getProduct(item.productId);
subtotal += product.price * item.quantity;
}
const discount = order.memberId ? subtotal * 0.1 : 0;
const tax = (subtotal - discount) * 0.1;
const total = subtotal - discount + tax;
// 3. ポイント計算(20 行)
let points = 0;
if (order.memberId) {
const member = getMember(order.memberId);
points = Math.floor(total * member.pointRate);
}
// 4. 在庫更新(15 行)
for (const item of order.items) {
const product = getProduct(item.productId);
product.stock -= item.quantity;
updateProduct(product);
}
// 5. オーダー保存(10 行)
const savedOrder = saveOrder({
...order,
total,
points,
status: 'confirmed'
});
// 6. メール送信(5 行)
sendOrderConfirmationEmail(savedOrder);
return { success: true, orderId: savedOrder.id };
}
// ✅ 良い例:小さく分割
function processOrder(order) {
if (!hasStock(order.items)) {
return { success: false, message: '在庫不足' };
}
const totals = calculateOrderTotals(order);
const points = calculateRewardPoints(order, totals.total);
updateStock(order.items);
const savedOrder = saveOrder({
...order,
total: totals.total,
points,
status: 'confirmed'
});
sendOrderConfirmationEmail(savedOrder);
return { success: true, orderId: savedOrder.id };
}
function hasStock(items) {
return items.every(item => {
const product = getProduct(item.productId);
return product.stock >= item.quantity;
});
}
function calculateOrderTotals(order) {
const subtotal = order.items.reduce((sum, item) => {
const product = getProduct(item.productId);
return sum + product.price * item.quantity;
}, 0);
const discount = order.memberId ? subtotal * 0.1 : 0;
const tax = (subtotal - discount) * 0.1;
const total = subtotal - discount + tax;
return { subtotal, discount, tax, total };
}
function calculateRewardPoints(order, total) {
if (!order.memberId) return 0;
const member = getMember(order.memberId);
return Math.floor(total * member.pointRate);
}
function updateStock(items) {
items.forEach(item => {
const product = getProduct(item.productId);
product.stock -= item.quantity;
updateProduct(product);
});
}
1 つのことを行う
【単一責任の原則】
関数は 1 つの責任だけを持つべき
【見分け方】
・関数名が「〜して、〜する」なら複数のことをしている可能性
・if/else のネストが深い
・ループ内に異なる処理が混在
// ❌ 悪い例:複数の責任
function saveUserAndSendEmail(user) {
// データベース保存
const savedUser = db.users.insert(user);
// メール送信
const emailContent = `ようこそ、${user.name} さん`;
sendEmail(user.email, 'ようこそ', emailContent);
// ログ記録
logger.info(`User created: ${savedUser.id}`);
// 通知送信
notifyAdmins(`New user: ${user.name}`);
return savedUser;
}
// ✅ 良い例:責任を分離
function saveUserAndSendEmail(user) {
const savedUser = saveUser(user);
sendWelcomeEmail(savedUser);
logUserCreation(savedUser);
notifyAdminsOfNewUser(savedUser);
return savedUser;
}
function saveUser(user) {
return db.users.insert(user);
}
function sendWelcomeEmail(user) {
const emailContent = `ようこそ、${user.name} さん`;
sendEmail(user.email, 'ようこそ', emailContent);
}
function logUserCreation(user) {
logger.info(`User created: ${user.id}`);
}
function notifyAdminsOfNewUser(user) {
notifyAdmins(`New user: ${user.name}`);
}
引数は少ないほど良い
【引数の数の目安】
・0 引数(無引数):理想的
・1 引数:良い
・2 引数:許容範囲
・3 引数:要注意(オブジェクト化を検討)
・4 引数以上:要リファクタリング
// ❌ 悪い例:引数が多すぎる
function createUser(email, password, firstName, lastName, dateOfBirth, phoneNumber, address, city, zipCode, country) {
// ...
}
// ✅ 良い例:オブジェクトでラップ
function createUser(userData) {
const {
email,
password,
firstName,
lastName,
dateOfBirth,
phoneNumber,
address
} = userData;
// ...
}
// 使用例
createUser({
email: 'test@example.com',
password: 'secure123',
firstName: 'Taro',
lastName: 'Yamada',
dateOfBirth: '1990-01-01',
phoneNumber: '090-1234-5678',
address: {
street: '...',
city: 'Tokyo',
zipCode: '100-0001',
country: 'Japan'
}
});
副作用を明示する
// ❌ 悪い例:隠れた副作用
function getUserById(id) {
// 実際はアクセスカウントも増加
const user = db.users.find(id);
db.userAccessLogs.insert({ userId: id, accessedAt: new Date() });
return user;
}
// 呼び出し元:「取得だけ」と思っている
// ✅ 良い例:副作用を明示
function getUserByIdAndLogAccess(id) {
const user = db.users.find(id);
logUserAccess(id);
return user;
}
// または、副作用を分離
function getUserById(id) {
return db.users.find(id);
}
function logUserAccess(id) {
db.userAccessLogs.insert({ userId: id, accessedAt: new Date() });
}
// 呼び出し元が明示的に両方呼ぶ
const user = getUserById(id);
logUserAccess(id);
コメントの使い方
コメントではなくコードで語る
// ❌ 悪い例:コメントで説明
// 18 歳未満なら false を返す
function checkAge(user) {
if (user.age < 18) {
return false;
}
return true;
}
// ✅ 良い例:関数名で意図を表現
function isAdult(user) {
return user.age >= 18;
}
// ❌ 悪い例:何をしているか説明
// 合計を計算する
let total = 0;
for (const item of items) {
total += item.price * item.quantity;
}
// ✅ 良い例:メソッド抽出
const total = calculateTotal(items);
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
適切なコメントの種類
【必要なコメント】
1. なぜ(Why):ビジネスロジックの理由
2. 意図(Intent):アルゴリズムの目的
3. 警告(Warning):副作用や制約
4. 補足(Amplification):重要な情報の強調
【不要なコメント】
1. 何をしているか(What):コード自体が語るべき
2. 自明な説明:変数名と同じ内容を繰り返す
3. コメントアウトされたコード:git で管理する
// ✅ 良い例:「なぜ」を説明
// 税法改正により、2024 年 4 月以降は軽減税率 8% を適用
// 参考:https://www.nta.go.jp/taxes/shiraberu/taxanswer/shohi/6801.htm
function calculateConsumptionTax(amount, date) {
if (date >= new Date('2024-04-01')) {
return amount * 0.08;
}
return amount * 0.10;
}
// ✅ 良い例:警告
// 注意:この関数は副作用としてキャッシュをクリアします
// 高頻度で呼び出さないこと(パフォーマンス劣化の恐れ)
function refreshUserData(userId) {
clearUserCache(userId);
return fetchUserFromDatabase(userId);
}
// ✅ 良い例:意図の説明
// 二分探索で O(log n) の検索を実現
// 配列は事前にソートされている前提
function binarySearch(sortedArray, target) {
let left = 0;
let right = sortedArray.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (sortedArray[mid] === target) return mid;
if (sortedArray[mid] < target) left = mid + 1;
else right = mid - 1;
}
return -1;
}
コメントアウトされたコードは削除
// ❌ 悪い例:コメントアウトされたコード
function calculateTotal(items) {
// const total = items.reduce((sum, item) => sum + item.price, 0);
// const total = 0;
// for (let i = 0; i < items.length; i++) {
// total += items[i].price;
// }
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
// ✅ 良い例:クリーン
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
理由:git で履歴を追えば、過去のコードはいつでも参照できます。
フォーマットと可読性
コードの整形
【フォーマットの原則】
・チームで一貫したスタイルを使用
・自動フォーマッターを活用(Prettier、gofmt、black など)
・手動で整形しない(自動化する)
// ❌ 悪い例:一貫性がない
function calculateTotal(items){
let total=0;
for(const item of items){
total+=item.price*item.quantity;
}
return total;
}
function getUser(id) {
const user = db.find(id);
return user;
}
// ✅ 良い例:一貫したスタイル
function calculateTotal(items) {
let total = 0;
for (const item of items) {
total += item.price * item.quantity;
}
return total;
}
function getUser(id) {
const user = db.find(id);
return user;
}
縦の構成
【関連コードの近接配置】
・関連する変数・関数は近くに配置
・使用する順に上から下に
// ❌ 悪い例:関連コードが離れている
class Order {
constructor() {
this.items = [];
}
calculateTax() {
// ...
}
get items() {
return this._items;
}
get total() {
// items を使っているが、定義から遠い
}
}
// ✅ 良い例:関連コードを近くに
class Order {
constructor() {
this._items = [];
}
get items() {
return this._items;
}
get total() {
// items の直後なので関連性が明確
}
calculateTax() {
// total の近くで.tax を計算
}
}
横の構成
【1 行の長さ】
・目安:80-120 文字
・超える場合は改行を検討
// ❌ 悪い例:長すぎる行
const result = users.filter(user => user.age >= 18 && user.isActive && user.hasPermission('read') && user.department === 'engineering').map(user => ({ id: user.id, name: user.name }));
// ✅ 良い例:適切に改行
const result = users
.filter(user => user.age >= 18)
.filter(user => user.isActive)
.filter(user => user.hasPermission('read'))
.filter(user => user.department === 'engineering')
.map(user => ({ id: user.id, name: user.name }));
グループ化と空白行
// ✅ 良い例:論理的なグループ化
// インポート
import express from 'express';
import cors from 'cors';
// 定数
const PORT = 3000;
const DEFAULT_TIMEOUT = 5000;
// ミドルウェア
const app = express();
app.use(cors());
app.use(express.json());
// ルート
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
// 起動
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
エラーハンドリング
例外を使う
// ❌ 悪い例:エラーコード
function divide(a, b) {
if (b === 0) {
return { error: 'DIVISION_BY_ZERO', result: null };
}
return { error: null, result: a / b };
}
const result = divide(10, 0);
if (result.error) {
// エラー処理
} else {
// 正常系
}
// ✅ 良い例:例外
function divide(a, b) {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
try {
const result = divide(10, 0);
// 正常系
} catch (error) {
// エラー処理
}
未チェック例外をスローする
// ❌ 悪い例:汎化例外
throw new Error('Something went wrong');
// ✅ 良い例:具体的な例外
class InsufficientStockError extends Error {
constructor(productId, requested, available) {
super(`Insufficient stock for product ${productId}: requested ${requested}, available ${available}`);
this.name = 'InsufficientStockError';
this.productId = productId;
this.requested = requested;
this.available = available;
}
}
throw new InsufficientStockError('prod_123', 10, 5);
try-catch-finally の適切な使用
// ✅ 良い例:リソースの確実な解放
let connection;
try {
connection = createDatabaseConnection();
const result = connection.query(sql);
return result;
} catch (error) {
logger.error('Database query failed', error);
throw error;
} finally {
if (connection) {
connection.close(); // エラーの有無にかかわらず実行
}
}
// ✅ モダンな写法(with ステートメント相当)
async function readFile(path) {
const file = await fs.open(path, 'r');
try {
return await file.readFile();
} finally {
await file.close();
}
}
エラーメッセージは具体的に
// ❌ 悪い例:不十分なメッセージ
throw new Error('Error');
throw new Error('Failed');
// ✅ 良い例:具体的なメッセージ
throw new Error(`User not found: userId=${userId}`);
throw new Error(`Payment failed: reason=${reason}, transactionId=${transactionId}`);
// ✅ さらに良い例:構造化
class PaymentError extends Error {
constructor(message, { transactionId, reason, amount }) {
super(message);
this.transactionId = transactionId;
this.reason = reason;
this.amount = amount;
}
}
throw new PaymentError('Payment failed', {
transactionId: 'txn_123',
reason: 'insufficient_funds',
amount: 10000
});
テスト可能なコード
テストのしやすさで判断
【テスト可能なコードの特徴】
・依存関係が注入されている
・副作用が分離されている
・決定論的(同じ入力 → 同じ出力)
・小さな関数に分割されている
// ❌ 悪い例:テストしにくい
function sendWelcomeEmail(email) {
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT
});
return transporter.sendMail({
from: 'noreply@example.com',
to: email,
subject: 'Welcome!',
text: 'Welcome to our service'
});
}
// テスト:実際に SMTP サーバーが必要?
// ✅ 良い例:テストしやすい
class EmailService {
constructor(transporter) {
this.transporter = transporter;
}
async sendWelcomeEmail(email) {
return this.transporter.sendMail({
from: 'noreply@example.com',
to: email,
subject: 'Welcome!',
text: 'Welcome to our service'
});
}
}
// テスト:モックを使用可能
const mockTransporter = {
sendMail: jest.fn().mockResolvedValue({ messageId: 'test' })
};
const service = new EmailService(mockTransporter);
await service.sendWelcomeEmail('test@example.com');
expect(mockTransporter.sendMail).toHaveBeenCalled();
依存関係の注入
// ❌ 悪い例:ハードコードされた依存
class UserService {
constructor() {
this.db = new Database(); // 結合度が強い
this.emailService = new EmailService();
}
async createUser(userData) {
const user = await this.db.insert(userData);
await this.emailService.sendWelcomeEmail(user.email);
return user;
}
}
// ✅ 良い例:依存性注入
class UserService {
constructor(db, emailService) {
this.db = db;
this.emailService = emailService;
}
async createUser(userData) {
const user = await this.db.insert(userData);
await this.emailService.sendWelcomeEmail(user.email);
return user;
}
}
// 本番環境
const db = new Database();
const emailService = new EmailService();
const userService = new UserService(db, emailService);
// テスト環境
const mockDb = { insert: jest.fn() };
const mockEmailService = { sendWelcomeEmail: jest.fn() };
const testUserService = new UserService(mockDb, mockEmailService);
純粋関数を活用
// 純粋関数:同じ入力 → 同じ出力、副作用なし
// ✅ 良い例:テストしやすい
// 純粋関数
function calculateTax(amount, taxRate) {
return amount * taxRate;
}
// テスト
test('calculateTax calculates 10% tax', () => {
expect(calculateTax(1000, 0.1)).toBe(100);
expect(calculateTax(1000, 0.1)).toBe(100); // べき等
});
// 不純な部分(副作用)を分離
async function saveOrderWithTax(order) {
const tax = calculateTax(order.amount, 0.1); // 純粋関数
const total = order.amount + tax;
// 副作用:データベース保存
await db.orders.insert({ ...order, tax, total });
return { orderId: '123', tax, total };
}
リファクタリングの実践
ボーイスカウトルール
【ボーイスカウトルール】
「来た時よりもきれいにしろ」
【実践方法】
・コードを触るたびに、少しでも改善する
・変数名を少しよくする
・関数を 1 つ抽出する
・コメントを 1 つ削除する
【効果】
・技術的負債の増加を防止
・大きなリファクタリング不要
・継続的な品質向上
リファクタリングのタイミング
// ❌ 悪い例:後回し
// 「時間がないから後でリファクタリングしよう」
// → 永遠にリファクタリングされない
// ✅ 良い例:その場で改善
// 1. 機能追加前に、既存コードを少し整理
// 2. バグ修正時に、関連コードも改善
// 3. コードレビュー時に、小さなリファクタリングを提案
3 つのルール
【Robert C. Martin の 3 つのルール】
1. 汚いコードを見つけたら、すぐきれいにする
2. 自分が入った時より少しきれいにして去る
3. 3 回同じことをしたら、リファクタリングする
・1 回目:気づく
・2 回目:パターンに気づく
・3 回目:共通メソッドに抽出する
クリーンコードのメリット
短期的メリット
【開発中のメリット】
・コードの意図がすぐ分かる
・デバッグ時間が短縮
・新規機能の追加が容易
・コードレビューがスムーズ
長期的メリット
【メンテナンスのメリット】
・属人化の防止(誰でも保守可能)
・オンボーディング時間の短縮
・回帰バグの減少
・技術的負債の蓄積抑制
・開発速度の低下防止
経済的効果
【研究による効果】
・コード読む時間:70% 削減(良好な命名の場合)
・バグ修正コスト:1/10(早期発見)
・新規機能追加時間:50% 短縮
【投資対効果】
クリーンコードを書く時間:10-20% 増加
長期的メンテナンスコスト:50-70% 削減
→ 総合的な生産性:向上
まとめ
クリーンコードの核心:
- 意味のある命名: 名前自体がドキュメント
- 小さな関数: 1 つのことを行う、10 行以内
- コードで語る: コメントよりコード
- 一貫したフォーマット: 自動フォーマッター活用
- 明示的エラーハンドリング: 具体的な例外
- テスト容易性: 依存注入、純粋関数
「クリーンコードは、読む人への愛情である」
クリーンコードを書くことは、将来の自分自身、そしてチームメンバーへの贈り物です。
一晩でマスターできる技術ではありませんが、日々の実践で確実に向上します。
今日から始められること:
- 変数名を少し具体的にしてみる
- 長い関数を 1 つ抽出してみる
- コメントを 1 つ削除してみる
小さな一歩が、やがて大きな違いを生みます。
参考資料
- 「クリーンコード——達人プログラマーの技術」Robert C. Martin 著
- 「The Clean Coder——プロフェッショナルプログラマーとしての流儀」Robert C. Martin 著
- 「Clean Architecture——達人に学ぶソフトウェアの構造と設計」Robert C. Martin 著
- 「リーダブルコード——より良いコードを書くためのシンプルな設計」Diomidis Spinellis 著
- Google Engineering Practices: https://google.github.io/eng-practices/
- Airbnb JavaScript Style Guide: https://airbnb.io/javascript/
免責事項 — 掲載情報は執筆時点のものです。料金・機能は変更される場合があります。最新情報は各公式サイトをご確認ください。