🏚️ レガシーコードとの戦い:最初の絶望
レガシーコードの典型的な例として、3年前に書かれた2万行のJavaScriptファイルでした。コメントはほとんどなく、関数名は「func1」「process2」といった意味不明なもの。TypeScriptへの移行も検討されていましたが、「リスクが高すぎる」という理由で誰も手をつけていませんでした。
💀 レガシーコードの特徴
- 巨大な単一ファイル:2万行を超える「main.js」に全ロジックが詰め込まれている
- グローバル変数の乱用:50個以上のグローバル変数が相互に依存
- テストコード皆無:1つもユニットテストが存在しない
- 密結合:どこを変更しても他の機能が壊れるかわからない恐怖
- ドキュメント不足:コードを書いた本人も既に退職
しかし、ジェンスパーク(Genspark)との協働により、段階的なリファクタリングのアプローチが有効です。この記事では、その具体的な戦略と手法を共有します。
🎯 リファクタリング戦略:5段階アプローチ
フェーズ1:現状把握と可視化
リファクタリングの最初のステップは、コードを理解することです。ジェンスパーク(Genspark)に「このコードベースを分析してください」と依頼しました。
// ジェンスパーク(Genspark)への指示:
「以下のファイルを分析し、以下の情報を抽出してください:
1. 関数一覧とその依存関係
2. グローバル変数の使用箇所
3. 重複コードのパターン
4. 循環依存の検出
5. コードの複雑度(Cyclomatic Complexity)
6. 潜在的なバグの指摘」
// ジェンスパークの分析結果(一部抜粋)
/*
分析結果:
- 総関数数: 127個
- グローバル変数: 53個
- 平均関数長: 82行
- 最長関数: processData() (412行)
- 重複コード: 23個のパターン(合計1,200行が重複)
- 循環依存: 12箇所
- 高複雑度関数: 18個(複雑度 > 20)
最優先で修正すべき箇所:
1. processData()関数 (412行, 複雑度42)
2. updateUI()関数 (387行, 複雑度38)
3. グローバル変数 'state' の使用 (98箇所で参照)
*/
✅ 可視化ツールの活用
ジェンスパーク(Genspark)に「Mermaid形式で依存関係図を生成してください」と依頼すると、コードの構造が一目で理解できるようになりました。
```mermaid
graph TD
A[main.js] --> B[processData]
A --> C[updateUI]
B --> D[fetchData]
B --> E[validateData]
B --> F[transformData]
C --> G[renderTable]
C --> H[renderChart]
D --> I[API Client]
E --> D
F --> E
G --> B
H --> B
style B fill:#ff6b6b
style C fill:#ff6b6b
```
赤色の関数が循環依存を持つ問題箇所です。
フェーズ2:テストコードの追加(Golden Master Testing)
リファクタリング前に、既存の動作を保証するテストコードが必要です。しかし、レガシーコードに対して詳細なユニットテストを書くのは非現実的です。
ここでGolden Master Testingというアプローチを採用しました。
// Golden Master Testingの実装
// ジェンスパーク(Genspark)への指示:
「既存のprocessData関数に対して、様々な入力パターンで実行し、
その出力を"Golden Master"(正解データ)として保存するテストを作成してください。
リファクタリング後、同じ入力で同じ出力が得られることを検証します。」
import fs from 'fs';
import crypto from 'crypto';
interface GoldenMasterTest {
name: string;
input: unknown;
output: unknown;
hash: string;
}
class GoldenMasterTester {
private goldenMasterPath = './tests/golden-masters.json';
private goldenMasters: GoldenMasterTest[] = [];
constructor() {
if (fs.existsSync(this.goldenMasterPath)) {
this.goldenMasters = JSON.parse(
fs.readFileSync(this.goldenMasterPath, 'utf-8')
);
}
}
// 初回実行:Golden Masterを記録
record(name: string, fn: (input: unknown) => unknown, input: unknown) {
const output = fn(input);
const hash = this.hashOutput(output);
this.goldenMasters.push({ name, input, output, hash });
fs.writeFileSync(
this.goldenMasterPath,
JSON.stringify(this.goldenMasters, null, 2)
);
console.log(`✓ Golden Master recorded: ${name}`);
}
// リファクタリング後:出力を検証
verify(name: string, fn: (input: unknown) => unknown) {
const master = this.goldenMasters.find(m => m.name === name);
if (!master) {
throw new Error(`Golden Master not found: ${name}`);
}
const output = fn(master.input);
const hash = this.hashOutput(output);
if (hash !== master.hash) {
console.error('❌ Output changed!');
console.error('Expected:', master.output);
console.error('Actual:', output);
throw new Error(`Golden Master verification failed: ${name}`);
}
console.log(`✓ Golden Master verified: ${name}`);
}
private hashOutput(output: unknown): string {
const json = JSON.stringify(output, Object.keys(output).sort());
return crypto.createHash('sha256').update(json).digest('hex');
}
}
// 使用例:Golden Masterの記録
const tester = new GoldenMasterTester();
// リファクタリング前に様々なパターンで記録
tester.record('processData-empty', processData, []);
tester.record('processData-single', processData, [{ id: 1, name: 'Test' }]);
tester.record('processData-multiple', processData, [
{ id: 1, name: 'Test1' },
{ id: 2, name: 'Test2' },
]);
tester.record('processData-special-chars', processData, [
{ id: 1, name: 'Test<>"' },
]);
tester.record('processData-large-dataset', processData, generateLargeDataset(1000));
// リファクタリング後に検証
tester.verify('processData-empty', processDataRefactored);
tester.verify('processData-single', processDataRefactored);
// ...以下同様に全パターンを検証
💡 Golden Master Testingのメリット
- 迅速な導入:既存コードの内部構造を理解せずにテスト可能
- 包括的なカバレッジ:関数の全体的な動作を検証
- リファクタリングの安全網:出力が変わっていないことを保証
- リグレッション検出:意図しない動作変更を即座に発見
フェーズ3:小さな改善の積み重ね(Strangler Fig Pattern)
一度に全てをリファクタリングするのは危険です。Strangler Fig Patternを採用し、少しずつ新しいコードで置き換えていきます。
// ステップ1:既存関数をラップする新しい関数を作成
// ジェンスパーク(Genspark)への指示:
「processData関数をラップする新しいprocessDataV2関数を作成してください。
内部では一旦processDataを呼び出し、徐々に新しい実装に置き換えていきます。」
// 既存コード(触らない)
function processData(data) {
// ...412行の複雑なロジック
}
// 新しいラッパー関数
function processDataV2(data) {
// フィーチャーフラグで段階的に切り替え
if (featureFlags.useNewProcessor) {
// 新しい実装(少しずつ増やしていく)
const validated = validateDataV2(data);
if (validated) {
return transformDataV2(validated);
}
// フォールバック:新実装で処理できない場合は旧実装を使用
console.warn('Falling back to legacy processData');
}
// 既存実装を呼び出し
return processData(data);
}
// ステップ2:部分的に新しい実装に置き換え
function processDataV2(data) {
// データバリデーション部分は新実装に移行
const validated = validateDataV2(data);
if (featureFlags.useNewTransformer) {
return transformDataV2(validated);
}
// 変換処理はまだ旧実装を使用
return processData(validated);
}
// ステップ3:完全に新しい実装に移行
function processDataV2(data) {
const validated = validateDataV2(data);
const transformed = transformDataV2(validated);
return enrichDataV2(transformed);
}
// ステップ4:旧実装を削除し、関数名を戻す
// processDataV2 → processData にリネーム
// 古いprocessDataは削除
🎉 Strangler Fig Patternの効果
このアプローチで実現できたこと:
- ゼロダウンタイム:本番環境で常に動作し続けながらリファクタリング
- 段階的リリース:各段階でGolden Master Testで検証
- 即座のロールバック:問題があればフィーチャーフラグで即座に旧実装に戻せる
- チーム全体の安心感:「壊れるかもしれない」恐怖から解放
フェーズ4:依存関係の整理とモジュール化
グローバル変数を削減し、適切な依存関係注入(DI)を実装します。
// ❌ リファクタリング前:グローバル変数に依存
var globalState = {
userData: null,
config: {},
cache: {},
};
function fetchUserData(userId) {
// グローバル変数を直接参照
if (globalState.cache[userId]) {
return globalState.cache[userId];
}
const data = fetch(`/api/users/${userId}`);
globalState.cache[userId] = data;
globalState.userData = data;
return data;
}
function updateUI() {
// グローバル変数を直接参照
const user = globalState.userData;
document.getElementById('username').textContent = user.name;
}
// ✅ リファクタリング後:依存関係注入
// ジェンスパーク(Genspark)への指示:
「グローバル変数への依存を排除し、依存関係注入パターンで実装してください。
各クラスは必要な依存を明示的にコンストラクタで受け取るようにしてください。」
interface UserRepository {
getUser(userId: string): Promise<User>;
saveUser(user: User): Promise<void>;
}
interface CacheService {
get<T>(key: string): T | null;
set<T>(key: string, value: T): void;
}
// リポジトリ実装
class UserRepositoryImpl implements UserRepository {
constructor(
private apiClient: ApiClient,
private cache: CacheService
) {}
async getUser(userId: string): Promise<User> {
// キャッシュチェック
const cached = this.cache.get<User>(`user:${userId}`);
if (cached) {
return cached;
}
// API呼び出し
const user = await this.apiClient.get<User>(`/users/${userId}`);
// キャッシュに保存
this.cache.set(`user:${userId}`, user);
return user;
}
async saveUser(user: User): Promise<void> {
await this.apiClient.put(`/users/${user.id}`, user);
// キャッシュを更新
this.cache.set(`user:${user.id}`, user);
}
}
// UI更新クラス
class UserProfileUI {
constructor(
private userRepository: UserRepository,
private container: HTMLElement
) {}
async render(userId: string): Promise<void> {
const user = await this.userRepository.getUser(userId);
this.container.innerHTML = `
<div class="user-profile">
<h2>${user.name}</h2>
<p>${user.email}</p>
</div>
`;
}
}
// 依存関係の組み立て(Dependency Injection Container)
class DIContainer {
private cache: CacheService;
private apiClient: ApiClient;
private userRepository: UserRepository;
constructor() {
// シングルトンインスタンスを作成
this.cache = new InMemoryCacheService();
this.apiClient = new ApiClient('/api');
this.userRepository = new UserRepositoryImpl(this.apiClient, this.cache);
}
getUserRepository(): UserRepository {
return this.userRepository;
}
createUserProfileUI(container: HTMLElement): UserProfileUI {
return new UserProfileUI(this.userRepository, container);
}
}
// 使用例
const container = new DIContainer();
const userProfileUI = container.createUserProfileUI(
document.getElementById('profile-container')
);
userProfileUI.render('user-123');
🔧 依存関係整理のメリット
- テスタビリティ:モック/スタブを注入して単体テストが容易に
- 再利用性:同じロジックを異なる環境で再利用可能
- 保守性:依存関係が明示的で変更の影響範囲が明確
- 柔軟性:実装を差し替えやすい(例:メモリキャッシュ → Redisキャッシュ)
フェーズ5:TypeScript化と型安全性の向上
最終段階として、JavaScriptからTypeScriptへの移行を実施しました。
// ジェンスパーク(Genspark)への指示:
「以下のJavaScriptコードをTypeScriptに変換してください。
厳格な型定義を使用し、any型は使用しないでください。
必要に応じてユーティリティ型(Partial, Readonly, Pickなど)を活用してください。」
// JavaScript(型情報なし)
function mergeUserData(existing, updates) {
return {
...existing,
...updates,
updatedAt: new Date(),
};
}
// TypeScript(型安全)
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
createdAt: Date;
updatedAt: Date;
}
// Readonlyで不変性を保証
type UserUpdate = Partial<Omit<User, 'id' | 'createdAt' | 'updatedAt'>>;
function mergeUserData(
existing: Readonly<User>,
updates: UserUpdate
): User {
return {
...existing,
...updates,
updatedAt: new Date(),
};
}
// コンパイル時に型エラーを検出
const user: User = {
id: '123',
name: 'John',
email: '
[email protected]',
role: 'user',
createdAt: new Date('2023-01-01'),
updatedAt: new Date('2023-01-01'),
};
// ✅ 正常:許可されたフィールドのみ更新
const updated = mergeUserData(user, { name: 'Jane' });
// ❌ コンパイルエラー:idは更新できない
// const invalid = mergeUserData(user, { id: '456' });
// ❌ コンパイルエラー:存在しないフィールド
// const invalid2 = mergeUserData(user, { age: 30 });
// ❌ コンパイルエラー:roleは限定された値のみ
// const invalid3 = mergeUserData(user, { role: 'superuser' });
✨ TypeScript移行の効果測定
3ヶ月のリファクタリング完了後の効果:
- バグ削減:本番環境でのバグ発生率の大幅な減少
- 開発速度向上:新機能開発のリードタイムが大幅な短縮
- コード行数:20,000行から12,000行に削減(重複排除)
- テストカバレッジ:0%から大幅に向上
- チームの自信:「コードを変更するのが怖い」という声がゼロに
🤖 ジェンスパーク(Genspark)の効果的な活用法
リファクタリングタスクの指示テンプレート
// パターン1:関数の分割
「以下の関数は複雑度が高すぎます。単一責任の原則に従って、
適切なサイズの関数に分割してください。各関数は以下の条件を満たすこと:
- 1関数あたり20行以内
- 複雑度10以下
- 1つの明確な責任のみを持つ
- 意味のある関数名をつける」
// パターン2:重複コードの統合
「以下のファイル群から重複しているロジックを検出し、
共通関数として抽出してください。抽出する際は:
- 汎用的なパラメータ化
- 適切なデフォルト引数の設定
- JSDocコメントで使用例を記載」
// パターン3:条件分岐の簡略化
「以下のネストした条件分岐を、早期リターンパターンとガード節を使って
フラットな構造に書き換えてください。可読性を最優先にしてください。」
// パターン4:エラーハンドリングの追加
「以下の関数に包括的なエラーハンドリングを追加してください:
- 入力バリデーション
- try-catchブロック
- 適切なエラーメッセージ
- ログ出力
- リトライ機能(必要に応じて)」
// パターン5:非同期処理の改善
「以下のコールバック地獄をasync/awaitで書き換えてください。
並行実行可能な処理はPromise.allで最適化してください。」
💡 ジェンスパーク(Genspark)とのペアリファクタリングのコツ
- 小さく始める:一度に1つの関数から始め、徐々に範囲を拡大
- 検証を徹底:各ステップでテストを実行し、動作を確認
- 意図を明確に:「なぜリファクタリングするのか」を伝える
- 制約を指定:パフォーマンス要件や互換性要件を明示
- レビューを重ねる:生成されたコードを必ず人間がレビュー
📊 リファクタリングの進捗管理
メトリクスでコード品質を可視化
ジェンスパーク(Genspark)に「コード品質メトリクスを計測するスクリプトを作成してください」と依頼し、進捗を定量的に追跡しました。
// code-metrics.ts
import { readFileSync } from 'fs';
import { glob } from 'glob';
interface CodeMetrics {
totalLines: number;
codeLines: number;
commentLines: number;
functionCount: number;
averageFunctionLength: number;
maxFunctionLength: number;
complexFunctions: number; // 複雑度 > 10
testCoverage: number;
}
async function calculateMetrics(pattern: string): Promise<CodeMetrics> {
const files = await glob(pattern);
let totalLines = 0;
let codeLines = 0;
let commentLines = 0;
let functionCount = 0;
let functionLengths: number[] = [];
for (const file of files) {
const content = readFileSync(file, 'utf-8');
const lines = content.split('\n');
totalLines += lines.length;
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith('//') || trimmed.startsWith('/*')) {
commentLines++;
} else if (trimmed.length > 0) {
codeLines++;
}
}
// 関数の検出と行数カウント
const functionRegex = /function\s+\w+|const\s+\w+\s*=\s*\(/g;
const functions = content.match(functionRegex) || [];
functionCount += functions.length;
// ...関数の長さを計算
}
return {
totalLines,
codeLines,
commentLines,
functionCount,
averageFunctionLength: functionLengths.reduce((a, b) => a + b, 0) / functionCount,
maxFunctionLength: Math.max(...functionLengths),
complexFunctions: 0, // 要実装
testCoverage: 0, // jest coverageから取得
};
}
// 週次でメトリクスを記録
async function trackProgress() {
const metrics = await calculateMetrics('src/**/*.ts');
console.log('=== Code Quality Metrics ===');
console.log(`Total Lines: ${metrics.totalLines}`);
console.log(`Code Lines: ${metrics.codeLines}`);
console.log(`Comment Rate: ${(metrics.commentLines / metrics.totalLines * 100).toFixed(1)}%`);
console.log(`Function Count: ${metrics.functionCount}`);
console.log(`Avg Function Length: ${metrics.averageFunctionLength.toFixed(1)} lines`);
console.log(`Max Function Length: ${metrics.maxFunctionLength} lines`);
console.log(`Test Coverage: ${metrics.testCoverage}%`);
}
💫 まとめ:リファクタリングは継続的な改善プロセス
レガシーコードのリファクタリングは、一度きりのイベントではなく継続的な改善プロセスです。
ジェンスパーク(Genspark)との協働により、開発チームは「完璧を目指すのではなく、昨日より良いコードにする」という姿勢でリファクタリングを進めました。その結果、チーム全体がコード品質向上に前向きになり、技術的負債を恐れずに立ち向かう文化が生まれました。
この記事で紹介した5段階アプローチ(現状把握 → テスト追加 → 段階的改善 → モジュール化 → 型安全性向上)は、どのようなプロジェクトにも適用できる普遍的な戦略です。
今日からできること:まずは最も変更頻度の高い1つのファイルを選び、ジェンスパークに「このファイルの問題点を分析してください」と依頼してみてください。その分析結果が、あなたのリファクタリングジャーニーの第一歩になるはずです。