Single Responsibility Principle (SRP) に基づいた大規模リファクタリング、コードの可読性とメンテナンス性の向上について
コードの可読性とメンテナンス性を向上させるため、Single Responsibility Principle (SRP) に基づいた大規模リファクタリングを実施しました。この記事では、リファクタリングの背景、実装の詳細、そして学んだ教訓について解説します。
開発初期段階では、機能追加を優先していたため、以下のような問題が発生していました:
chat_handler.pyが約2,641行、rule_based_recommendation.pyが約1,580行Single Responsibility Principle (SRP) に基づき、各モジュールが単一の責務を持つようにリファクタリング:
Before: 約200行(ビュー定義を含む)
After: 約89行(アプリ作成・設定・エラーハンドラー登録・Blueprint登録・起動処理のみ)
# app.py(リファクタリング後)
"""
Flask アプリケーションエントリポイント
責務: アプリ作成、設定(CORS・セッション・DB初期化)、エラーハンドラー登録、
Blueprint の import と register、起動処理のみ。
"""
import logging
import os
from flask import Flask
from flask_cors import CORS
from config.app_config import load_env, configure_logging, get_cors_config
from src.services.database import init_database
from src.handlers.error_handlers import register_error_handlers
configure_logging()
load_env()
app = Flask(__name__)
app.secret_key = os.getenv('SECRET_KEY', 'dev-secret-key')
# CORS・セッション設定
cors_config = get_cors_config()
CORS(app, **cors_config)
# データベース初期化
init_database()
# エラーハンドラーを登録
register_error_handlers(app, session, VERSION)
# Blueprint登録
from src.routes import create_main_routes, create_admin_routes, create_api_routes
app.register_blueprint(create_main_routes())
app.register_blueprint(create_admin_routes())
app.register_blueprint(create_api_routes())
if __name__ == '__main__':
app.run(debug=True, port=5000)
Before: すべてのルートがapp.pyに定義
After: 各ルートモジュールに分離
# src/routes/main_routes.py
from flask import Blueprint, render_template
def create_main_routes():
"""
メインルートのBlueprintを作成
"""
bp = Blueprint('main', __name__)
@bp.route('/')
def index():
return render_template('index.html')
@bp.route('/about')
def about():
return render_template('about.html')
return bp
# src/routes/admin_routes.py
from flask import Blueprint, render_template
def create_admin_routes():
"""
管理画面ルートのBlueprintを作成
"""
bp = Blueprint('admin', __name__, url_prefix='/admin')
@bp.route('/')
def admin_dashboard():
return render_template('admin/dashboard.html')
return bp
Before: 約1,580行の巨大なファイル
After: 以下のように分割
src/core/recommendation/
├── rule_based_recommendation.py # オーケストレーション(約200行)
├── recommendation_constants.py # 定数定義
├── life_stage_preference.py # ライフステージ別の優先度
├── symptom_pattern_matcher.py # 症状パターンマッチング
├── recommendation_finalizer.py # 推奨結果の最終化
├── recommendation_scoring.py # スコアリング
├── ingredient_diversity.py # 成分多様性の確保
└── final_score_calculator.py # 最終スコア計算
# src/core/recommendation/rule_based_recommendation.py(リファクタリング後)
"""
ルールベース推奨システムのオーケストレーション
責務: 推奨フローの統括、各モジュールの呼び出し
"""
from src.core.recommendation.life_stage_preference import apply_life_stage_preferences
from src.core.recommendation.symptom_pattern_matcher import match_symptom_patterns
from src.core.recommendation.recommendation_scoring import calculate_scores
from src.core.recommendation.recommendation_finalizer import finalize_recommendations
def recommend_medicines(nlu_result, user_info, medicine_df):
"""
医薬品を推奨(オーケストレーション)
"""
# 1. 症状パターンマッチング
matched_patterns = match_symptom_patterns(nlu_result)
# 2. ライフステージ別の優先度適用
candidates = apply_life_stage_preferences(candidates, user_info)
# 3. スコアリング
scored_candidates = calculate_scores(candidates, nlu_result, user_info)
# 4. 推奨結果の最終化
final_recommendations = finalize_recommendations(scored_candidates)
return final_recommendations
Before: 約2,641行の巨大なファイル
After: 以下のように分割
src/handlers/chat/
├── chat_handler.py # オーケストレーション(約300行)
├── chat_input_validator.py # 入力検証・ブロック
├── chat_response_builder.py # レスポンス構築
├── chat_triage.py # トリアージ処理
├── chat_counseling_flow.py # カウンセリングフロー
├── chat_recommendation_flow.py # 推奨フロー
├── chat_manual_reply.py # 手動返信処理
├── chat_emergency_handler.py # 緊急事案処理
├── chat_diagnosis_handler.py # 診断名処理
├── chat_store_inquiry.py # 店舗案内処理
└── chat_triage_follow_ups.py # トリアージフォローアップ
# src/handlers/chat/chat_handler.py(リファクタリング後)
"""
チャットハンドラーのオーケストレーション
責務: チャットリクエストの統括、各モジュールの呼び出し
"""
from src.handlers.chat.chat_input_validator import validate_and_block_input
from src.handlers.chat.chat_triage import triage_user_input
from src.handlers.chat.chat_recommendation_flow import handle_recommendation_flow
from src.handlers.chat.chat_counseling_flow import handle_counseling_flow
def handle_chat_request(user_message, session, request, sid):
"""
チャットリクエストを処理(オーケストレーション)
"""
# 1. 入力検証
sanitized_message, error_response = validate_and_block_input(
session, request, user_message, sid
)
if error_response:
return error_response
# 2. トリアージ
triage_result = triage_user_input(sanitized_message, session)
# 3. カテゴリに応じた処理
if triage_result['category'] == 'Physical':
return handle_recommendation_flow(sanitized_message, session, sid)
elif triage_result['category'] == 'Emotional':
return handle_counseling_flow(sanitized_message, session, sid)
# ... その他のカテゴリ
return default_response
Before: 約215行(OpenAIクライアント初期化と医薬品推奨が混在)
After: 以下のように分割
src/core/medicine/
├── medicine_logic.py # エントリポイント(約50行)
├── openai_client.py # OpenAIクライアント初期化
├── medicine_recommendation_gpt.py # GPTによる推奨
└── medicine_response_builder.py # レスポンス構築
# src/core/openai_client.py
"""
OpenAIクライアントの初期化
責務: OpenAIクライアントの作成と設定
"""
import openai
import os
_openai_client = None
def get_openai_client():
"""
OpenAIクライアントを取得(シングルトン)
"""
global _openai_client
if _openai_client is None:
_openai_client = openai.OpenAI(
api_key=os.getenv('OPENAI_API_KEY')
)
return _openai_client
Before: 約104行(テンプレート・ログ・プロンプト・生成が混在)
After: 以下のように分割
src/services/counseling/
├── counseling_response.py # ファサード(約30行)
├── counseling_templates.py # テンプレート定義
├── counseling_logger.py # ログ記録
├── counseling_prompts.py # プロンプト定義
├── counseling_generator.py # 返信生成
├── counseling_questions.py # 質問生成
├── counseling_satisfaction.py # 満足度評価
├── counseling_summary.py # 要約生成
├── counseling_topic_shift.py # 話題転換
├── counseling_mode_control.py # モード制御
└── counseling_processor.py # プロセッサ
開発・リファクタ用の補助スクリプト:
scripts/
├── build_api_routes.py # APIルートの自動生成
├── extract_*.py # コード抽出スクリプト
└── remove_*_views.py # ビュー削除スクリプト
アプリケーション本体(実行時にimportされる):
src/
├── core/ # コア機能
├── handlers/ # ハンドラー
├── routes/ # ルート定義
├── services/ # サービス層
├── utils/ # ユーティリティ
├── security/ # セキュリティ
└── analysis/ # 分析機能
原因: モジュール間の依存関係が循環している
解決策:
原因: ファイルの移動によりインポートパスが変更
解決策:
原因: リファクタリングによりテストが古くなる
解決策:
機能追加を優先しすぎると、後でリファクタリングが困難になる。定期的なリファクタリングが重要。
1つのモジュールが複数の責務を持つと、変更の影響範囲が広がる。SRPを徹底することで、変更の影響を局所化できる。
リファクタリング前にテストを実行し、リファクタリング後もテストが通ることを確認する。
一度にすべてをリファクタリングするのではなく、段階的に進めることで、リスクを最小化できる。
リファクタリングは、時間がかかり、大変な作業でした。しかし、リファクタリング後は、コードの可読性とメンテナンス性が大幅に向上し、開発効率が向上しました。
学んだこと:
リファクタリングを通じて、**「属人化しない仕組みを作る」**ことの重要性を改めて実感しました。一人の理解に依存しないよう、責務分離・ログ・テスト・READMEを重視しています。
リファクタリングは、一度きりの作業ではありません。継続的にコードの品質を向上させ、メンテナンス性を高めていく必要があります。
SRPに基づいたリファクタリングにより、コードの可読性とメンテナンス性を大幅に向上させました。各モジュールが単一の責務を持つことで、変更の影響範囲が明確になり、テストと機能追加が容易になりました。
「属人化しない仕組みを作る」 - この信念を胸に、今後もコード品質の向上とリファクタリングを継続していきます。