「このメソッドの循環的複雑度(Cyclomatic Complexity)が60です」——人間がこの数字を見たら、リファクタリングに着手する気力が湧かないかもしれません。しかしClaude Codeに任せると、1〜2時間で60→19に下げてくれます。
本記事では、3つのデータ処理エンジン(A・B・C)のコアモジュールの複雑度を段階的に低減した実践と、AIにリファクタリングを依頼する際のコツを記録します。
循環的複雑度(CC)とは
循環的複雑度(Cyclomatic Complexity、CC)は、プログラムの制御フローの複雑さを数値化したものです。分岐(if/elif/else)やループ(for/while)が増えるほど値が大きくなります。
1# CC = 1(分岐なし)2def add(a: int, b: int) -> int:3 return a + b4
5# CC = 3(if + elif)6def classify(score: int) -> str:7 if score >= 80:8 return "A"9 elif score >= 60:10 return "B"11 else:12 return "C"13
14# CC = 15+(実際の本番コードから抜粋・簡略化)15def process_signal(signal, source_data, config, resources, ...):7 collapsed lines
16 if signal.type == "EXECUTE":17 if source_data.external_metric > 35:18 if config.fallback_enabled:19 if len(resources) < config.max_resources:20 if signal.score > config.min_score:21 if source_data.session == "BATCH_A":22 # ... さらにネストが深くなる一般的な目安:
| CC | 評価 |
|---|---|
| 1〜10 | シンプル、テストしやすい |
| 11〜20 | やや複雑、注意が必要 |
| 21〜50 | 複雑、テストが困難 |
| 50以上 | テスト不能、リファクタリング必須 |
対象となった3つのモジュール
H-COMP-01: データ処理エンジンA 統合分析モジュール
1ファイル: unified_analyzer.py2対象メソッド: _analyze_pair()3Before: CC = 604After: CC = 19(-68%)5手法: 5メソッドに分割_analyze_pair()は「1つのデータペアの全指標を計算する」メソッドでした。411指標の計算ロジックが1メソッドに詰め込まれており、if文とfor文が多重にネストしていました。
H-COMP-02: データ処理エンジンB
1ファイル: processing_engine.py2対象メソッド: __init__() と _process_batch_breakout()3Before: __init__() CC = 53, _process_batch_breakout() CC = 514After: __init__() CC = 21(-60%), _process_batch_breakout() CC = 6(-88%)5手法: 4メソッド分割、14サブメソッド化__init__()にCC 53という数値は「初期化処理にビジネスロジックが混入している」サインです。
H-COMP-03: データ処理エンジンC
1ファイル: processing_engine.py2対象メソッド: run_session() と _monitor_resources()3Before: run_session() CC = 42, _monitor_resources() CC = 244After: run_session() CC = 23(-45%), _monitor_resources() CC = 10(-58%)5手法: 4メソッド分割、10サブメソッド化AIへの依頼方法
基本プロンプト
1unified_analyzer.py の _analyze_pair() メソッドの循環的複雑度を下げてください。2
3制約:4- 外部のインターフェース(引数・戻り値)を変更しないこと5- 既存のテストが全てPASSすること6- 新しいクラスは作成しないこと(メソッド分割のみ)7- 分割したメソッドにはプレフィックス _calc_ を付けることなぜ制約が重要か
制約なしで「リファクタリングしてください」と言うと、AIは以下をやりがちです。
- 新しいクラスを作る — 必要のない抽象化が増える
- インターフェースを変更する — 呼び出し元の全修正が必要になる
- デザインパターンを導入する — Strategy pattern、Visitor patternなど、コードベースに馴染まない構造が入る
「外部インターフェースを変えない」「メソッド分割のみ」という制約で、安全にCCを下げられます。
分割のパターン
パターン1:条件分岐のメソッド抽出
1# Before(CC寄与: +5)2def process(self, data):3 if data.type == "A":4 # 20行のロジック5 elif data.type == "B":6 # 20行のロジック7 elif data.type == "C":8 # 20行のロジック9
10# After(CC寄与: +3、各サブメソッドは+1)11def process(self, data):12 if data.type == "A":13 self._process_type_a(data)14 elif data.type == "B":15 self._process_type_b(data)2 collapsed lines
16 elif data.type == "C":17 self._process_type_c(data)分岐の数は変わりませんが、各分岐の「中身」がサブメソッドに分離されることで、テスト可能性が大幅に向上します。
パターン2:ガード節の早期リターン
1# Before(深いネスト、CC寄与: +4)2def execute(self, signal, config):3 if signal is not None:4 if signal.score > config.min_score:5 if config.enabled:6 if not self.is_circuit_breaker_active():7 # 実際の処理8
9# After(ガード節で早期リターン、同じCC値だがネストが浅い)10def execute(self, signal, config):11 if signal is None:12 return13 if signal.score <= config.min_score:14 return15 if not config.enabled:4 collapsed lines
16 return17 if self.is_circuit_breaker_active():18 return19 # 実際の処理(ネスト0段)パターン3:計算ロジックのまとめ出し
1# Before(__init__内にビジネスロジック)2def __init__(self, config):3 self.config = config4 # 50行の初期化ロジック5 # パラメータ検証、デフォルト値設定、依存関係の解決...6
7# After8def __init__(self, config):9 self.config = config10 self._validate_config()11 self._setup_defaults()12 self._resolve_dependencies()テスト戦略
リファクタリング時のテスト戦略は「回帰テストで既存の動作を保証する」が基本です。
回帰テスト
各エンジンのリファクタリング結果:
| エンジン | 構造検証テスト | 回帰テスト | 結果 |
|---|---|---|---|
| データ処理エンジンA | 20件 | 4,119件 | 全PASS |
| データ処理エンジンB | 35件 | 2,064件 | 全PASS |
| データ処理エンジンC | 33件 | 1,270件 | 全PASS |
「構造検証テスト」は、分割後のメソッドが正しく呼び出されることを確認するテストです。「回帰テスト」は、リファクタリング前後で動作が変わらないことを確認します。
AIに回帰テストを先に実行させる
1リファクタリングの手順:21. まず現在の全テストを実行して、PASS状態を確認する32. リファクタリングを実施する43. 全テストを再実行して、PASS状態が維持されていることを確認する54. 新しい構造検証テストを追加するこの順序をCLAUDE.mdに書いておくことで、AIが「リファクタリングしたがテストを壊した」パターンを防げます。
コード重複削減との連携
複雑度低減と並行して、3つのエンジン間のコード重複も削減しました。
1BatchEngine(バッチ処理実行エンジン):2 Before: 3モード × 1,247行 = 3,741行3 After: 基底クラス1,787行 + 3モード差分 = 1,787行(52%削減)4 手法: Template Method パターン(基底クラス + 9つのフックメソッド)Template Method パターンは「全体の処理フローは基底クラスが定義し、モード固有の処理だけをサブクラスがオーバーライドする」設計です。
1class BaseBatchEngine:2 def run(self, pairs: list[str]) -> BatchResult:3 data = self._load_data(pairs) # フック14 signals = self._generate_signals(data) # フック25 results = self._execute_tasks(signals) # フック36 return self._evaluate(results) # フック47
8class StreamBatchEngine(BaseBatchEngine):9 def _load_data(self, pairs):10 # Stream固有のデータ読み込み(4時間間隔)11 ...12
13class IntervalBatchEngine(BaseBatchEngine):14 def _load_data(self, pairs):15 # Interval固有のデータ読み込み(1時間間隔)1 collapsed line
16 ...学んだこと
1. AIはリファクタリングが得意
「メソッド分割」はAIにとって機械的な作業であり、ミスが少ないです。人間がやると「ついでにロジックも変えてしまう」リスクがありますが、AIは「制約通りに分割だけする」のが得意です。
2. CC 20以下を目標にする
CC 20以下なら、メソッドの動作を人間が頭の中でトレースできます。CC 50以上になると、変更を加えるたびに予測不能な副作用が生じます。
3. 回帰テストが安全網
7,000件以上の回帰テストが全PASSすることが、「リファクタリングで動作が変わっていない」ことの証拠になります。テストなしのリファクタリングは危険すぎます。
4. __init__()のCC50超は設計の問題
コンストラクタの複雑度が高いのは、初期化処理にビジネスロジックが混入しているサインです。初期化は「設定の保存」と「依存関係の解決」だけにして、ビジネスロジックは別メソッドに分離すべきです。
まとめ
AIリファクタリングで重要なのは以下の3点です。
- 制約の明示: 「外部インターフェース変更なし」「メソッド分割のみ」「新クラス作成禁止」を指定し、AIの過剰な設計変更を防止します
- 回帰テスト必須: 7,000件以上のテストで動作の不変性を保証します。テストなしのリファクタリングは禁止です
- CC 20以下を目標: CC 60→19(-68%)、CC 53→21(-60%)、CC 51→6(-88%)と、全3エンジンで達成しました
「複雑度が高いコードは怖くて触れない」——その恐怖をAI + 回帰テストが解消します。