以下の内容はhttps://kafkafinancialgroup.hatenablog.com/entry/2025/10/03/194408より取得しました。


Pythonプロジェクトを救う実践的リファクタリング:デザインパターンとDI導入による技術的負債の返済

はじめに:成長するプロジェクトの宿命

Pythonプロジェクトは最初は小さく始まります。しかし、機能追加を繰り返すうちに、いつの間にか750行の巨大なmain.py、6段階のif-elif地獄、至るところに散らばるグローバル変数——こうした「技術的負債」が蓄積していきます。[2][5]

本記事では、実際の大規模リファクタリングプロジェクトを題材に、デザインパターン依存性注入(DI)を活用した実践的な改善手法を解説します。[1][7]

リファクタリングの黄金律

まず、最も重要な原則を押さえましょう:[1]

1. テストなしのリファクタリングはギャンブル

「動いているコードを触るな」という格言は半分正しく、半分間違っています。正しいのは、テストなしでの改変がリスクであるという点です。リファクタリング前に最低限の統合テストを用意し、動作が変わらないことを継続的に検証しましょう。[5][1]

2. 小さく、段階的に進める

一度に全てを書き換えようとすると失敗します。以下のフェーズ分けが効果的です:[5][1]

3. リファクタリングと機能追加を混ぜない

リファクタリング中は外部動作を変えないことに専念します。新機能は別ブランチで開発し、構造改善後にマージしましょう。[1][5]

実践例1:Strategy Patternで巨大関数を分割

Before:750行のGod Object

def main(mode: str, theme: str, keyword: str):
    # 100行の初期化処理
    api_setup()
    config_load()
    
    # 200行のテーマ抽出ロジック
    if mode == "specific":
        theme_data = extract_by_keyword(keyword)
    elif mode == "trending":
        theme_data = fetch_trending()
    # ... 他のモード
    
    # 150行のスクリプト生成
    script = crew.generate_script(theme_data)
    
    # 200行の音声合成
    audio = tts.synthesize(script)
    
    # 100行の動画生成と後処理
    video = generate_video(audio, script)
    upload(video)

この巨大関数は、単一責任原則に違反しており、テストやデバッグが困難です。[3][2]

After:Strategy Patternによる分離

class WorkflowStep(ABC):
    @abstractmethod
    def execute(self, context: WorkflowContext) -> WorkflowContext:
        pass

class ThemeExtractionStep(WorkflowStep):
    def execute(self, context):
        context.theme = self._extract_theme(context.mode)
        return context

class ScriptGenerationStep(WorkflowStep):
    def execute(self, context):
        context.script = self.crew.generate(context.theme)
        return context

class Workflow:
    def __init__(self, steps: List[WorkflowStep]):
        self.steps = steps
    
    def run(self, context: WorkflowContext):
        for step in self.steps:
            context = step.execute(context)
        return context

メリット:[7][2] - 各ステップが独立してテスト可能 - 新しいステップの追加が容易(開放閉鎖原則) - エラー発生箇所の特定が簡単

実践例2:Chain of Responsibilityでfallback地獄を解消

Before:6段階のif-elif連鎖

def synthesize_speech(text: str):
    if API_KEY_OPENAI:
        try:
            return openai_tts(text)
        except:
            pass
    
    if API_KEY_ELEVENLABS:
        try:
            return elevenlabs_tts(text)
        except:
            pass
    
    # ... 他4つのプロバイダー
    
    raise RuntimeError("All TTS providers failed")

この構造は、新しいプロバイダー追加時に全体を書き換える必要があり、保守性が低いです。[2][3]

After:Chain of Responsibility

class TTSProvider(ABC):
    def __init__(self):
        self.next_provider: Optional[TTSProvider] = None
    
    def set_next(self, provider: TTSProvider):
        self.next_provider = provider
        return provider
    
    @abstractmethod
    def _synthesize(self, text: str) -> Optional[bytes]:
        pass
    
    def synthesize(self, text: str) -> bytes:
        result = self._synthesize(text)
        if result:
            return result
        if self.next_provider:
            return self.next_provider.synthesize(text)
        raise RuntimeError(f"{self.__class__.__name__} failed")

class OpenAITTSProvider(TTSProvider):
    def _synthesize(self, text: str):
        if not self.api_key:
            return None
        try:
            return openai.audio.speech.create(...)
        except Exception as e:
            logger.warning(f"OpenAI TTS failed: {e}")
            return None

# 使用例
chain = OpenAITTSProvider()
chain.set_next(ElevenLabsTTSProvider()).set_next(GoogleTTSProvider())
audio = chain.synthesize(text)

メリット:[7] - 新しいプロバイダーはTTSProviderを継承するだけ - 優先順位の変更が設定ファイルで可能 - 各プロバイダーの独立したテストが容易

実践例3:DIコンテナでグローバル変数を排除

Before:グローバル変数の濫用

# モジュールロード時に即座に初期化
sheets_manager = SheetsManager() if GOOGLE_SHEET_ID else None
discord_notifier = DiscordNotifier()
metadata_storage = MetadataStorage()

def upload_video(path: str):
    if sheets_manager:  # グローバル変数に依存
        sheets_manager.log_upload(path)
    discord_notifier.notify(f"Uploaded: {path}")

この構造は、テスト時のモック注入が困難で、起動時に不要なサービスまで初期化されます。[2][7]

After:DIコンテナによる遅延評価

class AppContainer:
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance
    
    @property
    def sheets_manager(self):
        if self._sheets_manager is None:
            if settings.google_sheet_id:
                self._sheets_manager = SheetsManager()
        return self._sheets_manager
    
    def set_sheets_manager(self, manager: SheetsManager):
        """テスト用のモック注入"""
        self._sheets_manager = manager

# 使用例
container = AppContainer()

def upload_video(path: str):
    if container.sheets_manager:
        container.sheets_manager.log_upload(path)

メリット:[7] - 必要時のみ初期化(起動速度向上) - テストでのcontainer.set_sheets_manager(mock)が簡単 - 全依存関係がAppContainerに集約され、可視性向上

よくある落とし穴と対策

落とし穴1:Monkey Patchの濫用

# 避けるべきパターン
original_function = library.function
library.function = patched_function  # グローバル状態を破壊

対策:Adapter Patternでラップし、依存性注入で切り替え可能にする。[7]

落とし穴2:「後で削除」のProxyパターン

後方互換性のためのProxyは、明確な削除計画がないと永久に残存します。Deprecation警告を追加し、削除期限を明記しましょう。[5]

落とし穴3:重複コードの放置

動画生成処理が3箇所に重複している場合、1箇所の修正が他に波及しません。Strategy Patternで共通処理を抽出しましょう。[2]

改善メトリクスの追跡

リファクタリングの成果を可視化することで、チームの納得感が得られます:

  • コード削減率:750行 → 390行(48%削減)
  • Lint警告:per-file-ignores 16ファイル → 8ファイル
  • テストカバレッジ:ユニット13ファイル、統合4ファイル(要改善)
  • 追加されたパターン:Strategy、Chain of Responsibility、DI、Singleton

次のステップ:テストなきリファクタリングの危険性

重要な警告:本記事で紹介したリファクタリングは構造改善に成功していますが、「テストなきリファクタリング」の段階にあります。以下のテストが不足しています:[1]

  1. 新しいWorkflowStepの統合テスト
  2. TTSProviderのfallback動作テスト
  3. DIコンテナのモック注入テスト

リファクタリング後は必ず統合テストとE2Eテストの追加に時間を割きましょう。[5]

まとめ

Pythonプロジェクトの技術的負債返済には、以下の戦略が有効です:

  1. デザインパターンの適用:Strategy、Chain of Responsibility、DIで責務を分離
  2. 段階的アプローチ:Phase 0(即削除)→ Phase 1(構造改善)→ Phase 2(残存問題)
  3. テストファーストリファクタリング前にテストを整備し、動作保証を確保[1]
  4. メトリクス追跡:行数削減、Lint警告、カバレッジで成果を可視化

適切なリファクタリングは、プロジェクトの保守性、テスト容易性、拡張性を劇的に向上させます。しかし、常に「テストなくして変更なし」の原則を守りましょう。[5][1]


参考リソース: - [Code Refactoring Best Practices – with Python Examples][1] - [8 Python Code Refactoring Techniques: Tools & Practices][2] - [Code Refactoring in 2025: Best Practices][5]

1 2 3 4 5 6 7 8 9




以上の内容はhttps://kafkafinancialgroup.hatenablog.com/entry/2025/10/03/194408より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14