Tasuke Hubのロゴ

ITを中心に困っている人を助けるメディア

分かりやすく解決策を提供することで、あなたの困ったをサポート。 全ての人々がスムーズに生活できる世界を目指します。

レガシーコード改善の実践ガイド!安全にリファクタリングを進める5つのステップ

記事のサムネイル

レガシーコードとは何か?なぜ改善が必要なのか

レガシーコードとは、テストが不十分で変更に対するリスクが高いコードのことです。古いだけがレガシーコードではありません。以下のような特徴があります。

// レガシーコードの典型例
function processUserData(userData) {
    // ネストが深い
    if (userData) {
        if (userData.name) {
            if (userData.email) {
                if (userData.age > 0) {
                    // 複数の責任を持つ
                    var processedData = userData.name.toUpperCase();
                    var emailValid = userData.email.includes('@');
                    var ageCategory = userData.age > 18 ? 'adult' : 'minor';
                    
                    // グローバル変数への依存
                    globalDatabase.save({
                        name: processedData,
                        email: emailValid ? userData.email : null,
                        category: ageCategory
                    });
                    
                    return true;
                }
            }
        }
    }
    return false;
}

レガシーコード改善が必要な理由は明確です。開発速度の低下、バグの増加、メンテナンスコストの増大を防ぐためです。実際に、技術的負債が蓄積すると新機能開発にかかる時間が2倍〜3倍になることもあります。

安全なリファクタリングを始める前の準備

リファクタリングを始める前に、必ず以下の準備を行います。これにより、作業中のリスクを最小限に抑えられます。

まず、現在のコードの動作を確認するテストを作成します。テストがない場合は、最低限の「キャラクタライゼーションテスト」を書きましょう。

// 既存の動作を保護するテスト
describe('processUserData', () => {
    test('正常なユーザーデータで正しく処理される', () => {
        const userData = {
            name: 'john doe',
            email: '[email protected]',
            age: 25
        };
        
        const result = processUserData(userData);
        expect(result).toBe(true);
        // データベースへの保存も確認
        expect(globalDatabase.lastSaved).toEqual({
            name: 'JOHN DOE',
            email: '[email protected]',
            category: 'adult'
        });
    });
    
    test('不正なデータの場合は false を返す', () => {
        expect(processUserData(null)).toBe(false);
        expect(processUserData({})).toBe(false);
        expect(processUserData({ name: 'test' })).toBe(false);
    });
});

次に、バージョン管理システムで作業ブランチを作成し、小さな変更を頻繁にコミットする体制を整えます。リファクタリングは一度に大きく変更せず、段階的に進めることが重要です。

段階的リファクタリングの基本戦略

レガシーコードの改善は段階的に行います。一度に大きく変更すると、バグの原因を特定するのが困難になります。以下の順序で進めましょう。

まず、最も読みやすくできる部分から始めます。変数名の改善やネストの解消などです。

// Step 1: ネストを減らし、早期リターンを使用
function processUserData(userData) {
    if (!userData || !userData.name || !userData.email || userData.age <= 0) {
        return false;
    }
    
    // 複数の責任を持つ(まだ改善の余地あり)
    var processedData = userData.name.toUpperCase();
    var emailValid = userData.email.includes('@');
    var ageCategory = userData.age > 18 ? 'adult' : 'minor';
    
    globalDatabase.save({
        name: processedData,
        email: emailValid ? userData.email : null,
        category: ageCategory
    });
    
    return true;
}

次に、単一責任の原則に従って関数を分割します。

// Step 2: 責任を分離し、より小さな関数に分割
function validateUserData(userData) {
    return userData && 
           userData.name && 
           userData.email && 
           userData.age > 0;
}

function formatUserName(name) {
    return name.toUpperCase();
}

function validateEmail(email) {
    return email.includes('@');
}

function categorizeAge(age) {
    return age > 18 ? 'adult' : 'minor';
}

function processUserData(userData) {
    if (!validateUserData(userData)) {
        return false;
    }
    
    const processedName = formatUserName(userData.name);
    const isEmailValid = validateEmail(userData.email);
    const ageCategory = categorizeAge(userData.age);
    
    globalDatabase.save({
        name: processedName,
        email: isEmailValid ? userData.email : null,
        category: ageCategory
    });
    
    return true;
}

最後に、依存性の注入を行い、テストしやすい構造にします。これにより、グローバル変数への依存がなくなります。

テストが不十分なコードの改善アプローチ

テストがないレガシーコードを改善する際は、「シーム」を見つけて徐々にテスト可能な構造に変更していきます。シームとは、テストを書きやすくするために挿入できる境界のことです。

まず、外部依存を分離しましょう。データベース操作やAPI呼び出しなど、テストしにくい部分を抽象化します。

// テストしやすい構造に改善
class UserProcessor {
    constructor(database, validator) {
        this.database = database;
        this.validator = validator;
    }
    
    processUserData(userData) {
        if (!this.validator.isValid(userData)) {
            return false;
        }
        
        const processedUser = this.formatUser(userData);
        this.database.save(processedUser);
        return true;
    }
    
    formatUser(userData) {
        return {
            name: userData.name.toUpperCase(),
            email: this.validator.isEmailValid(userData.email) ? 
                   userData.email : null,
            category: userData.age > 18 ? 'adult' : 'minor'
        };
    }
}

// 依存関係の実装
class UserValidator {
    isValid(userData) {
        return userData && 
               userData.name && 
               userData.email && 
               userData.age > 0;
    }
    
    isEmailValid(email) {
        return email.includes('@');
    }
}

次に、テストでは依存関係をモックして、ロジックのみをテストします。

// テストしやすい構造のテスト
describe('UserProcessor', () => {
    let processor;
    let mockDatabase;
    let mockValidator;
    
    beforeEach(() => {
        mockDatabase = {
            save: jest.fn()
        };
        mockValidator = {
            isValid: jest.fn(),
            isEmailValid: jest.fn()
        };
        processor = new UserProcessor(mockDatabase, mockValidator);
    });
    
    test('有効なユーザーデータを正しく処理する', () => {
        const userData = { name: 'john', email: '[email protected]', age: 25 };
        mockValidator.isValid.mockReturnValue(true);
        mockValidator.isEmailValid.mockReturnValue(true);
        
        const result = processor.processUserData(userData);
        
        expect(result).toBe(true);
        expect(mockDatabase.save).toHaveBeenCalledWith({
            name: 'JOHN',
            email: '[email protected]',
            category: 'adult'
        });
    });
});

このアプローチにより、段階的にテストカバレッジを向上させながら、安全にリファクタリングを進められます。

依存関係の複雑さを解消する方法

複雑な依存関係は、レガシーコードの最大の問題の一つです。循環依存や密結合により、テストが困難で変更のリスクが高くなります。

まず、依存関係を可視化しましょう。以下のようなツールで依存関係グラフを作成できます。

# Node.jsプロジェクトの場合
npm install -g madge
madge --circular src/
madge --image dependencies.png src/

次に、依存性逆転の原則を適用して、依存関係を整理します。

// Before: 密結合で変更しにくい
class OrderService {
    constructor() {
        this.emailService = new EmailService(); // 直接依存
        this.database = new MySQLDatabase();    // 具象クラスに依存
        this.paymentGateway = new StripeGateway(); // 変更できない
    }
    
    processOrder(order) {
        // 複雑なビジネスロジック
        this.database.save(order);
        this.paymentGateway.charge(order.amount);
        this.emailService.sendConfirmation(order.email);
    }
}

// After: 疎結合でテストしやすい
class OrderService {
    constructor(database, paymentGateway, emailService) {
        this.database = database;           // インターフェースに依存
        this.paymentGateway = paymentGateway;
        this.emailService = emailService;
    }
    
    async processOrder(order) {
        try {
            await this.database.save(order);
            await this.paymentGateway.charge(order.amount);
            await this.emailService.sendConfirmation(order.email);
        } catch (error) {
            console.error('Order processing failed:', error);
            throw error;
        }
    }
}

ファクトリーパターンやDIコンテナを使用して、依存関係の管理を自動化します。

// DIコンテナを使った依存関係の管理
class Container {
    constructor() {
        this.services = new Map();
    }
    
    register(name, factory) {
        this.services.set(name, factory);
    }
    
    get(name) {
        const factory = this.services.get(name);
        return factory ? factory() : null;
    }
}

// 使用例
const container = new Container();

container.register('database', () => new MySQLDatabase());
container.register('paymentGateway', () => new StripeGateway());
container.register('emailService', () => new EmailService());

container.register('orderService', () => new OrderService(
    container.get('database'),
    container.get('paymentGateway'),
    container.get('emailService')
));

これにより、各コンポーネントを独立してテストでき、実装の変更も容易になります。

チーム全体でレガシーコード改善を継続する仕組み

レガシーコード改善は個人の努力だけでは限界があります。チーム全体で取り組む継続的な改善システムを構築することが重要です。

まず、技術的負債を定期的に評価し、優先順位をつけるプロセスを作ります。

// 技術的負債の評価指標例
const technicalDebtMetrics = {
    complexity: {
        cyclomaticComplexity: 15, // 10以下が理想
        nestingDepth: 5,          // 3以下が理想
        functionLength: 50        // 20行以下が理想
    },
    testability: {
        testCoverage: 60,         // 80%以上が目標
        mocksCount: 8,            // 5個以下が理想
        dependencies: 12          // 7個以下が理想
    },
    maintainability: {
        duplicatedCode: 15,       // 5%以下が目標
        todoComments: 23,         // 計画的に削減
        lastModified: '2023-01-15' // 古いコードを特定
    }
};

次に、リファクタリングを日常的な開発プロセスに組み込みます。「ボーイスカウト・ルール」を適用し、コードを触る際は必ず少しでも改善して帰ります。

// プルリクエストテンプレートに含める項目
/*
## 技術的負債の改善
- [ ] 関数の複雑度を下げた
- [ ] テストカバレッジを向上させた
- [ ] 依存関係を整理した
- [ ] 重複コードを削除した
- [ ] 型安全性を向上させた

## 改善後の影響
- パフォーマンス: 向上/変化なし/低下
- 可読性: 向上/変化なし/低下  
- テスタビリティ: 向上/変化なし/低下
*/

定期的な技術的負債の棚卸しミーティングを開催し、チーム全体で改善方針を決定します。

# 技術的負債を可視化するツールの導入例
npm install --save-dev eslint-plugin-sonarjs
npm install --save-dev jscpd  # 重複コード検出
npm install --save-dev complexity-report

# package.jsonにスクリプトを追加
"scripts": {
    "debt:complexity": "complexity-report src/",
    "debt:duplication": "jscpd src/",
    "debt:lint": "eslint src/ --plugin sonarjs"
}

最後に、リファクタリングの成果を定期的に測定し、チームで共有することで、継続的な改善のモチベーションを維持します。技術的負債の削減によって開発速度が向上し、バグが減少することを実感できれば、チーム全体でレガシーコード改善に取り組む文化が根付きます。

レガシーコードの改善は一朝一夕にはできませんが、段階的なアプローチと継続的な取り組みによって、必ず成果を出すことができます。今日からでも小さな改善を始めてみましょう。

TH

Tasuke Hub管理人

東証プライム市場上場企業エンジニア

情報系修士卒業後、大手IT企業にてフルスタックエンジニアとして活躍。 Webアプリケーション開発からクラウドインフラ構築まで幅広い技術に精通し、 複数のプロジェクトでリードエンジニアを担当。 技術ブログやオープンソースへの貢献を通じて、日本のIT技術コミュニティに積極的に関わっている。

🎓情報系修士🏢東証プライム上場企業💻フルスタックエンジニア📝技術ブログ執筆者

このトピックはこちらの書籍で勉強するのがおすすめ!

この記事の内容をさらに深く理解したい方におすすめの一冊です。実践的な知識を身につけたい方は、ぜひチェックしてみてください!

おすすめ記事

おすすめコンテンツ