目次
この記事の内容
リファクタリングは、ソフトウェアの品質を維持し続けるための不可欠な活動です。本記事では、コードスメルの識別から、具体的なリファクタリング技法、安全性を担保する手順までを体系的に解説します。
リファクタリングの基本概念
リファクタリングとは
**リファクタリング(Refactoring)**とは、外部からの振る舞いを変えずに、コードの内部構造を整理し、理解しやすく、変更しやすい状態にすることです。
【リファクタリングの定義】
・外部振る舞い:不變(テストで保証)
・内部構造:改善(可読性、保守性、拡張性)
・目的:技術的負債の削減、開発速度の維持
リファクタリングと修正の違い
| 作業 | 目的 | 振る舞い | テスト |
|---|---|---|---|
| リファクタリング | 構造改善 | 不変 | 既存テストで保証 |
| バグ修正 | 不具合修正 | 変更(期待通り) | 回帰テスト追加 |
| 機能追加 | 新機能実装 | 追加 | 新規テスト追加 |
リファクタリングのメリット
【短期的メリット】
・コードの可読性向上
・バグの発見
・理解しやすくなる
【長期的メリット】
・保守コスト削減
・新機能追加が容易
・開発速度の低下防止
・技術的負債の蓄積抑制
コードスメル——リファクタリングの兆候
コードスメルとは
**コードスメル(Code Smell)**とは、コードの中に漂う「悪臭」であり、潜在的な問題を示す兆候です。バグそのものではありませんが、放置すると問題を引き起こします。
代表的なコードスメル 7 選
1. 重複コード(Duplicated Code)
【症状】
同じようなコードが複数箇所に存在
【問題点】
・修正漏れのリスク
・コード量増加
・一貫性維持の困難
【解決策】
・メソッド抽出
・クラス抽出
・テンプレートメソッドパターン
・ストラテジーパターン
// ❌ 重複コード
function calculateOrderTotal(items) {
let total = 0;
for (const item of items) {
total += item.price * item.quantity;
}
const tax = total * 0.1;
return total + tax;
}
function calculateCartTotal(items) {
let total = 0;
for (const item of items) {
total += item.price * item.quantity;
}
const tax = total * 0.1;
return total + tax;
}
// ✅ 共通メソッドに抽出
function calculateTotal(items) {
const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
const tax = subtotal * 0.1;
return subtotal + tax;
}
function calculateOrderTotal(items) {
return calculateTotal(items);
}
function calculateCartTotal(items) {
return calculateTotal(items);
}
2. 長いメソッド(Long Method)
【症状】
メソッドが 50 行、100 行を超える
【問題点】
・理解に時間がかかる
・デバッグが困難
・再利用できない
【解決策】
・メソッド抽出(Extract Method)
・一時変数の削減
・早期リターン
// ❌ 長いメソッド(100 行超)
function processOrder(order) {
// 1. 在庫確認
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. 価格計算
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. ポイント計算
let points = 0;
if (order.memberId) {
const member = getMember(order.memberId);
points = Math.floor(total * member.pointRate);
}
// 4. 在庫更新
for (const item of order.items) {
const product = getProduct(item.productId);
product.stock -= item.quantity;
updateProduct(product);
}
// 5. オーダー保存
const savedOrder = saveOrder({
...order,
total,
points,
status: 'confirmed'
});
// 6. メール送信
sendOrderConfirmationEmail(savedOrder);
return { success: true, orderId: savedOrder.id };
}
// ✅ 短いメソッドに分割
function processOrder(order) {
if (!checkStock(order.items)) {
return { success: false, message: '在庫不足' };
}
const { subtotal, discount, tax, total } = calculateOrderTotals(order);
const points = calculatePoints(order, total);
updateStock(order.items);
const savedOrder = saveOrder({
...order,
total,
points,
status: 'confirmed'
});
sendOrderConfirmationEmail(savedOrder);
return { success: true, orderId: savedOrder.id };
}
function checkStock(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 calculatePoints(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);
});
}
3. 巨大なクラス(Large Class)
【症状】
・1 つのクラスが 1000 行を超える
・責任が複数ある(神オブジェクト)
【問題点】
・理解が困難
・修正の影響範囲が広い
・テストが書きにくい
【解決策】
・クラス抽出(Extract Class)
・責務の分離(Single Responsibility Principle)
・コンポジションへの置き換え
4. 長すぎるパラメータリスト(Long Parameter List)
【症状】
メソッドのパラメータが 4-5 個を超える
【問題点】
・呼び出しが煩雑
・順序を間違えやすい
・変更に弱い
【解決策】
・パラメータオブジェクトの導入
・オブジェクト全体の受け渡し
// ❌ 長すぎるパラメータリスト
function createUser(email, password, firstName, lastName, dateOfBirth, phoneNumber, address) {
// ...
}
// ✅ パラメータオブジェクト
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: '東京都...'
});
5. データへの執着(Data Clumps)
【症状】
常に一緒に登場するデータのかたまり
【解決策】
・専用クラスにまとめる
// ❌ データの塊
function calculateShipping(weight, width, height, depth) {
// ...
}
// ✅ クラスにまとめる
class Dimensions {
constructor(weight, width, height, depth) {
this.weight = weight;
this.width = width;
this.height = height;
this.depth = depth;
}
get volumetricWeight() {
return (this.width * this.height * this.depth) / 5000;
}
get billableWeight() {
return Math.max(this.weight, this.volumetricWeight);
}
}
function calculateShipping(dimensions) {
const rate = getShippingRate(dimensions.billableWeight);
return dimensions.billableWeight * rate;
}
6. 重複した条件ロジック(Duplicate Conditional Logic)
【症状】
同じ条件式が複数箇所に出現
【解決策】
・条件のメソッド化
・ポリモーフィズムの活用
// ❌ 重複した条件
function getCharge(customer) {
if (customer.type === 'premium') {
return baseCharge * 0.8;
}
if (customer.type === 'business') {
return baseCharge * 0.9;
}
return baseCharge;
}
function getShippingRate(customer) {
if (customer.type === 'premium') {
return 0; // 送料無料
}
if (customer.type === 'business') {
return baseRate * 0.5;
}
return baseRate;
}
// ✅ ポリモーフィズム
class Customer {
getDiscountRate() { return 1.0; }
getShippingMultiplier() { return 1.0; }
}
class PremiumCustomer extends Customer {
getDiscountRate() { return 0.8; }
getShippingMultiplier() { return 0.0; }
}
class BusinessCustomer extends Customer {
getDiscountRate() { return 0.9; }
getShippingMultiplier() { return 0.5; }
}
7. コメントへの依存(Comments as Code Smell)
【症状】
・コードではなくコメントで説明している
・「なぜ」ではなく「何を」を説明している
【問題点】
・コメントとコードの乖離
・コメントのメンテナンスコスト
【解決策】
・コード自体を分かりやすくする
・メソッド名、変数名で意図を表現
// ❌ コメントで説明
// 未成年なら false を返す
function checkAge(user) {
if (user.age < 18) {
return false;
}
return true;
}
// ✅ コード自体を明確に
function isAdult(user) {
return user.age >= 18;
}
リファクタリング技法
1. メソッド抽出(Extract Method)
【目的】
長いメソッドを小さなメソッドに分割する
【手順】
1. 抽出するコードブロックを選択
2. 意味のある名前を付ける
3. ローカル変数の使用を確認
4. メソッドとして抽出
5. 呼び出し元に置き換え
// Before
function printOwing(invoice) {
let outstanding = 0;
// 明細の出力
console.log('*******************************');
console.log('**** 顧客のご請求額 ***********');
console.log('*******************************');
// 合計の計算
for (const o of invoice.outstanding) {
outstanding += o.amount;
}
// 期日の記録
const today = new Date();
invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
// 最終出力
console.log(`名前:${invoice.customer}`);
console.log(`金額:${outstanding}`);
console.log(`期日:${invoice.dueDate}`);
}
// After
function printOwing(invoice) {
printBanner();
const outstanding = calculateOutstanding(invoice);
recordDueDate(invoice);
printDetails(invoice, outstanding);
}
function printBanner() {
console.log('*******************************');
console.log('**** 顧客のご請求額 ***********');
console.log('*******************************');
}
function calculateOutstanding(invoice) {
return invoice.outstanding.reduce((sum, o) => sum + o.amount, 0);
}
function recordDueDate(invoice) {
const today = new Date();
invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
}
function printDetails(invoice, outstanding) {
console.log(`名前:${invoice.customer}`);
console.log(`金額:${outstanding}`);
console.log(`期日:${invoice.dueDate}`);
}
2. 変数の置き換え(Replace Variable)
【目的】
一時変数を削減し、コードをシンプルにする
【技法】
・一時変数の置き換え
・ループ変数の分離
・中間変数の削除
// Before
function getPrice(order) {
const basePrice = order.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
const discount = basePrice * order.discountRate;
const afterDiscount = basePrice - discount;
const tax = afterDiscount * 0.1;
const total = afterDiscount + tax;
return total;
}
// After
function getPrice(order) {
const basePrice = order.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
const afterDiscount = basePrice * (1 - order.discountRate);
return afterDiscount * 1.1; // 税込価格
}
3. クラスの抽出(Extract Class)
【目的】
巨大なクラスを責務ごとに分割する
【手順】
1. 責務を識別
2. 関連するフィールド・メソッドをグループ化
3. 新しいクラスを作成
4. 元のクラスから委譲
// Before
class Person {
constructor(name, email, phone, address, city, zip, country) {
this.name = name;
this.email = email;
this.phone = phone;
this.address = address;
this.city = city;
this.zip = zip;
this.country = country;
}
getFullAddress() {
return `${this.address}, ${this.city}, ${this.zip}, ${this.country}`;
}
sendEmail(message) {
// メール送信ロジック
}
sendSMS(message) {
// SMS 送信ロジック
}
}
// After
class ContactInfo {
constructor(email, phone, address, city, zip, country) {
this.email = email;
this.phone = phone;
this.address = address;
this.city = city;
this.zip = zip;
this.country = country;
}
getFullAddress() {
return `${this.address}, ${this.city}, ${this.zip}, ${this.country}`;
}
}
class Person {
constructor(name, contactInfo) {
this.name = name;
this.contact = contactInfo;
}
sendEmail(message) {
// メール送信ロジック
}
sendSMS(message) {
// SMS 送信ロジック
}
}
4. 継承から委譲へ(Replace Inheritance with Delegation)
【目的】
不適切な継承をコンポジションに置き換える
【継承が問題になる場合】
・実装の流用だけのために継承している
・スーパークラスの実装詳細に依存したくない
// ❌ 不適切な継承
class ArrayStack extends Array {
push(item) {
// 追加のバリデーション
if (!item) throw new Error('null は追加できません');
super.push(item);
}
}
// ✅ 委譲
class Stack {
constructor() {
this.items = [];
}
push(item) {
if (!item) throw new Error('null は追加できません');
this.items.push(item);
}
pop() {
return this.items.pop();
}
peek() {
return this.items[this.items.length - 1];
}
get size() {
return this.items.length;
}
}
5. 条件ロジックの単純化
【目的】
複雑な条件を分かりやすくする
【技法】
・ガード節(早期リターン)
・ポリモーフィズム
・テーブル駆動
ガード節
// ❌ ネストが深い
function calculateBonus(employee) {
if (employee.performance === 'excellent') {
if (!employee.hasWarnings) {
if (employee.years >= 5) {
return employee.salary * 0.3;
} else {
return employee.salary * 0.2;
}
} else {
return employee.salary * 0.1;
}
} else {
return 0;
}
}
// ✅ ガード節
function calculateBonus(employee) {
if (employee.performance !== 'excellent') return 0;
if (employee.hasWarnings) return employee.salary * 0.1;
if (employee.years >= 5) return employee.salary * 0.3;
return employee.salary * 0.2;
}
テーブル駆動
// ❌ 複雑な条件分岐
function getShippingCost(country, weight) {
if (country === 'JP') {
if (weight <= 1) return 500;
if (weight <= 5) return 800;
return 1200;
} else if (country === 'US') {
if (weight <= 1) return 1500;
if (weight <= 5) return 2500;
return 4000;
} else if (country === 'EU') {
if (weight <= 1) return 1800;
if (weight <= 5) return 3000;
return 5000;
}
throw new Error(`Unknown country: ${country}`);
}
// ✅ テーブル駆動
const SHIPPING_RATES = {
JP: [{ max: 1, cost: 500 }, { max: 5, cost: 800 }, { max: Infinity, cost: 1200 }],
US: [{ max: 1, cost: 1500 }, { max: 5, cost: 2500 }, { max: Infinity, cost: 4000 }],
EU: [{ max: 1, cost: 1800 }, { max: 5, cost: 3000 }, { max: Infinity, cost: 5000 }],
};
function getShippingCost(country, weight) {
const rates = SHIPPING_RATES[country];
if (!rates) throw new Error(`Unknown country: ${country}`);
const rate = rates.find(r => weight <= r.max);
return rate.cost;
}
6. API の単純化
【目的】
メソッドの使いやすさを向上させる
【技法】
・パラメータの追加・削除
・関数の名前変更
・オブジェクトの統合
// Before
function setAlert(customerId, alertType, isEmail, isSMS, isPush) {
// ...
}
// After
function setAlert(customerId, alertType, channels = ['email']) {
// ...
}
// 使用例
setAlert('cust123', 'promotion', ['email', 'sms', 'push']);
安全なリファクタリングの手順
2 つの帽子
【ケント・ベックの「2 つの帽子」】
・リファクタリング帽子:構造改善のみ(振る舞い不変)
・機能追加帽子:振る舞い変更(新機能・バグ修正)
→ 同時に被らない
・リファクタリング中:テストがすべて通っている状態を維持
・機能追加中:テストを追加・更新
リファクタリングの 3 ステップ
【ステップ 1: 準備】
1. 対象コードを特定
2. 既存テストの確認(カバレッジが十分か)
3. テストがなければ追加
4. 現在のテストをパスさせる
【ステップ 2: 実行】
1. 小さなステップで変更
2. 各ステップ後にテスト実行
3. 失敗したら即座に元に戻す
【ステップ 3: 検証】
1. 全テストを再実行
2. パフォーマンス確認(必要に応じて)
3. コードレビュー
リファクタリングチェックリスト
【リファクタリング前のチェックリスト】
□ 対象コードのテストは存在するか
□ テストは網羅的か(エッジケース含む)
□ テストは安定しているか(flaky ではない)
□ バージョン管理の準備はできているか
□ 影響範囲は把握したか
【リファクタリング中のチェックリスト】
□ 各ステップは小さいか(5-10 分以内)
□ テストは頻繁に実行しているか
□ 1 ステップで複数の変更をしていないか
□ 命名は適切か
【リファクタリング後のチェックリスト】
□ 全テストがパスしているか
□ コードは改善されたか
□ パフォーマンスは劣化していないか
□ ドキュメントは更新したか
テストとの関係
テストがリファクタリングを可能にする
【テストの重要性】
・テストがない = リファクタリングできない
・テストがある = 自信を持って変更できる
【テスト戦略】
1. ユニットテスト:各関数・メソッドの動作を保証
2. 統合テスト:コンポーネント間の連携を保証
3. 回帰テスト:既存機能が壊れていないことを保証
テストファーストのリファクタリング
【Red-Green-Refactor】
1. Red: 失敗するテストを書く
2. Green: テストをパスさせる(汚くても OK)
3. Refactor: コードを整理する
このサイクルで常にクリーンなコードを維持
リファクタリングに強いテストの書き方
// ❌ 実装詳細に依存したテスト
test('calculateTotal は reduce を使用する', () => {
// 実装方法に依存 → リファクタリングで失敗
});
// ✅ 振る舞いに焦点
test('calculateTotal は合計金額を計算する', () => {
const items = [
{ price: 100, quantity: 2 },
{ price: 50, quantity: 3 }
];
expect(calculateTotal(items)).toBe(350);
});
test('calculateTotal は空配列なら 0 を返す', () => {
expect(calculateTotal([])).toBe(0);
});
test('calculateTotal は負の数量を拒否する', () => {
expect(() => calculateTotal([{ price: 100, quantity: -1 }]))
.toThrow('数量は 0 以上である必要があります');
});
リファクタリングのタイミング
ルール 3: リファクタリングのタイミング
【ロバート・マーティンの「3 つのルール」】
1. 汚いコードを見つけたら、すぐきれいにする
2. 自分が入った時より少しきれいにして去る
3. 3 回同じことをしたら、リファクタリングする
【実践的なタイミング】
・コードレビュー中
・バグ修正時(関連箇所も整理)
・機能追加時(既存コードの整理)
・テスト追加時(テストしにくい = リファクタリング必要)
リファクタリングすべき時
【即座にリファクタリング】
・バグの原因がコードの複雑さ
・新規機能追加が困難
・理解に 30 分以上かかる
【計画的にリファクタリング】
・技術的負債が蓄積
・チーム全体の生産性低下
・パフォーマンスボトルネック
リファクタリングすべきでない時
【避けるべき状況】
・締め切り直前
・テストがないコード(まずテストを書く)
・仕様が確定していない
・パフォーマンスクリティカルな箇所(計測前に最適化しない)
【判断基準】
・ビジネス価値 > リファクタリングコスト
・緊急度が高い場合は後回し(技術的負債として記録)
リファクタリングの実践パターン
パターン 1: 機能移動に伴うリファクタリング
【状況】
新機能追加時に、既存コードが整理されていない
【アプローチ】
1. 既存コードをリファクタリング
2. 新機能を追加
3. テストで保証
// Before: 既存コードが複雑
function checkout(cart, user, paymentMethod, shippingAddress) {
// 100 行の複雑なロジック
}
// After: リファクタリング後
function checkout(cart, user, paymentMethod, shippingAddress) {
validateCart(cart);
validateUser(user);
const order = createOrder(cart, user);
const payment = processPayment(order, paymentMethod);
const shipment = createShipment(order, shippingAddress);
sendConfirmation(order, user);
return { order, payment, shipment };
}
// 新機能:ギフトオプション
function checkoutWithGift(cart, user, paymentMethod, shippingAddress, giftOptions) {
validateCart(cart);
validateUser(user);
const order = createOrder(cart, user);
const payment = processPayment(order, paymentMethod);
const shipment = createShipment(order, shippingAddress);
if (giftOptions) {
applyGiftWrapping(shipment, giftOptions);
addGiftMessage(shipment, giftOptions.message);
}
sendConfirmation(order, user);
return { order, payment, shipment };
}
パターン 2: 並行開発との調整
【状況】
チームメンバーが同じファイルを編集している
【アプローチ】
1. 機能ブランチでリファクタリング
2. 短いサイクルでマージ
3. チームに周知
パターン 3: 段階的リファクタリング
【状況】
大規模なリファクタリングが必要
【アプローチ】
1. 依存関係を分析
2. 小さなステップに分割
3. 段階的に適用
4. 各ステップでテスト
アンチパターン
❌ 1. 完璧主義リファクタリング
【症状】
・全てを完璧にしようとする
・リファクタリング中に機能追加を始める
・「ついでに」他の箇所も修正
【対策】
・範囲を限定
・1 つの変更で 1 つの目的
・「ついでに」を禁止
❌ 2. テストなしリファクタリング
【症状】
・テストを書かずにリファクタリング
・手動テストのみで確認
【リスク】
・回帰バグの発見が遅れる
・自信を持って変更できない
【対策】
・まずテスト
・カバレッジを可視化
❌ 3. パフォーマンスと混同
【症状】
・最適化をリファクタリングと呼ぶ
・計測せずに「遅そう」で変更
【違い】
・リファクタリング:構造改善(速度不変)
・最適化:速度改善(構造変更)
【対策】
・計測してボトルネックを特定
・目的を明確に分ける
❌ 4. 大規模リファクタリング
【症状】
・数週間の大規模リファクタリング
・他の機能開発を停止
【リスク】
・コンフリクト
・ビジネス機会の損失
・挫折
【対策】
・小さなステップに分割
・継続的にマージ
・ビジネス価値とバランス
ツール活用
自動リファクタリングツール
| 言語 | ツール | 機能 |
|---|---|---|
| JavaScript/TypeScript | VS Code, WebStorm | メソッド抽出、名前変更、移動 |
| Java | IntelliJ IDEA | 強力なリファクタリング機能 |
| Python | PyCharm, Rope | 名前変更、抽出、移動 |
| Go | gofmt, gopls | フォーマット、整理 |
| Rust | rust-analyzer | 名前変更、抽出 |
静的解析ツール
// ESLint(JavaScript)
module.exports = {
rules: {
'complexity': ['error', { max: 10 }], // 循環的複雑性
'max-lines-per-function': ['error', { max: 50 }],
'max-params': ['error', { max: 4 }],
'no-duplicate-imports': 'error',
}
};
コードメトリクス
【循環的複雑性(Cyclomatic Complexity)】
・if, else, for, while, case などの分岐数
・目安:10 以下
・10 超:リファクタリング検討
・20 超:必須
【認知複雑性(Cognitive Complexity)】
・コードの理解しやすさ
・目安:15 以下
まとめ
リファクタリングの核心:
- 定義の理解: 外部振る舞いを変えずに内部構造を改善
- コードスメルの識別: 重複、長さ、複雑さの兆候をキャッチ
- 技法の習得: 抽出、移動、単純化の定石
- 安全性の確保: テストで保証、小さなステップ
- タイミングの判断: 即座に、計画的に、バランスよく
「リファクタリングは、今日できる最も重要な投資の一つである」
リファクタリングは、一見「生産的でない」ように見えるかもしれません。しかし、リファクタリングを怠ったコードは、時間とともに理解しにくくなり、変更が困難になり、最終的には開発速度を著しく低下させます。
小さなリファクタリングを毎日続けることが、長期的な開発速度の維持、バグの減少、チームの満足度向上につながります。
参考資料
- 「リファクタリング——既存のコードを安全に改善する——」Martin Fowler, Kent Beck 著
- 「クリーンコード——達人プログラマーの技術」Robert C. Martin 著
- 「リファクタリング・ウェットウェア」Staffan Nöteberg 著
- Refactoring.guru: https://refactoring.guru/
- Martin Fowler’s Refactoring: https://martinfowler.com/tags/refactoring.html
免責事項 — 掲載情報は執筆時点のものです。料金・機能は変更される場合があります。最新情報は各公式サイトをご確認ください。