259 lines
11 KiB
Python
259 lines
11 KiB
Python
"""
|
||
Модуль DecisionEngine — классификация архитектурного риска и
|
||
выбор стратегии модернизации.
|
||
|
||
Реализует многокритериальный анализ на основе набора метрик,
|
||
полученных от MetricsEngine и DependencyAnalyzer.
|
||
|
||
Логика принятия решений:
|
||
Каждый модуль получает «очки риска» по нескольким независимым
|
||
критериям. Итоговый уровень риска определяется числом набранных
|
||
очков, что обеспечивает интерпретируемость и прозрачность решения.
|
||
|
||
Стратегии (по Lehman & Belady, 1985; Fowler, 2018):
|
||
LOW / MEDIUM → KEEP / REFACTOR
|
||
HIGH → REENGINEER
|
||
CRITICAL → REPLACE
|
||
"""
|
||
|
||
import logging
|
||
from typing import Dict, List, Tuple
|
||
|
||
from config import AnalyzerConfig, DEFAULT_CONFIG
|
||
from models import (
|
||
AnalysisReport,
|
||
ModuleDecision,
|
||
ModuleMetrics,
|
||
ModernizationStrategy,
|
||
RiskLevel,
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class DecisionEngine:
|
||
"""
|
||
Классифицирует модули по уровню архитектурного риска
|
||
и формирует рекомендации по стратегии модернизации.
|
||
"""
|
||
|
||
def __init__(self, config: AnalyzerConfig = DEFAULT_CONFIG):
|
||
self.config = config
|
||
self.t = config.thresholds
|
||
|
||
# ------------------------------------------------------------------
|
||
# Публичный API
|
||
# ------------------------------------------------------------------
|
||
|
||
def analyze(
|
||
self,
|
||
report: AnalysisReport,
|
||
cycles: List[List[str]],
|
||
) -> AnalysisReport:
|
||
"""
|
||
Заполняет report.decisions и системные агрегаты.
|
||
|
||
:param report: отчёт с заполненными modules
|
||
:param cycles: список циклов зависимостей от DependencyAnalyzer
|
||
:returns: обновлённый отчёт
|
||
"""
|
||
decisions: List[ModuleDecision] = []
|
||
|
||
for module_name, module in report.modules.items():
|
||
score, reasons = self._score_module(module)
|
||
risk = self._score_to_risk(score)
|
||
strategy = self._risk_to_strategy(risk)
|
||
|
||
decision = ModuleDecision(
|
||
module_name=module_name,
|
||
risk_level=risk,
|
||
strategy=strategy,
|
||
reasons=reasons,
|
||
priority=self._compute_priority(score, module),
|
||
)
|
||
decisions.append(decision)
|
||
|
||
# Сортируем по приоритету (меньше = важнее)
|
||
decisions.sort(key=lambda d: d.priority)
|
||
report.decisions = decisions
|
||
report.dependency_cycles = cycles
|
||
|
||
# Системные агрегаты
|
||
report.total_files = sum(len(m.files) for m in report.modules.values())
|
||
report.total_nloc = sum(m.total_nloc for m in report.modules.values())
|
||
report.total_functions = sum(m.total_functions for m in report.modules.values())
|
||
|
||
all_ccn = [
|
||
f.ccn
|
||
for m in report.modules.values()
|
||
for f in m.functions
|
||
]
|
||
report.avg_system_ccn = sum(all_ccn) / len(all_ccn) if all_ccn else 0.0
|
||
|
||
report.system_risk_level = self._system_risk(decisions)
|
||
report.recommended_system_strategy = self._risk_to_strategy(report.system_risk_level)
|
||
|
||
logger.info(
|
||
"DecisionEngine: системный риск=%s, стратегия=%s",
|
||
report.system_risk_level.value,
|
||
report.recommended_system_strategy.name,
|
||
)
|
||
|
||
return report
|
||
|
||
# ------------------------------------------------------------------
|
||
# Оценка отдельного модуля
|
||
# ------------------------------------------------------------------
|
||
|
||
def _score_module(self, module: ModuleMetrics) -> Tuple[int, List[str]]:
|
||
"""
|
||
Начисляет очки риска по каждому критерию.
|
||
Возвращает (суммарный_счёт, список_причин).
|
||
|
||
Каждый критерий даёт 1 (warn) или 2 (critical) очка.
|
||
"""
|
||
score = 0
|
||
reasons: List[str] = []
|
||
|
||
# --- Цикломатическая сложность ---
|
||
if module.max_ccn > self.t.ccn_critical:
|
||
score += 2
|
||
reasons.append(
|
||
f"Критическая CCN: max={module.max_ccn} (порог {self.t.ccn_critical})"
|
||
)
|
||
elif module.avg_ccn > self.t.ccn_warn:
|
||
score += 1
|
||
reasons.append(
|
||
f"Повышенная средняя CCN: avg={module.avg_ccn:.1f} (порог {self.t.ccn_warn})"
|
||
)
|
||
|
||
# --- Доля сложных функций ---
|
||
if module.heavy_functions_ratio > self.t.heavy_functions_ratio_critical:
|
||
score += 2
|
||
reasons.append(
|
||
f"Критическая доля сложных функций: "
|
||
f"{module.heavy_functions_ratio:.0%} (порог {self.t.heavy_functions_ratio_critical:.0%})"
|
||
)
|
||
elif module.heavy_functions_ratio > self.t.heavy_functions_ratio_warn:
|
||
score += 1
|
||
reasons.append(
|
||
f"Высокая доля сложных функций: "
|
||
f"{module.heavy_functions_ratio:.0%} (порог {self.t.heavy_functions_ratio_warn:.0%})"
|
||
)
|
||
|
||
# --- Исходящая связность (fan-out) ---
|
||
if module.coupling_out > self.t.coupling_out_critical:
|
||
score += 2
|
||
reasons.append(
|
||
f"Критическая исходящая связность: C_out={module.coupling_out} "
|
||
f"(порог {self.t.coupling_out_critical})"
|
||
)
|
||
elif module.coupling_out > self.t.coupling_out_warn:
|
||
score += 1
|
||
reasons.append(
|
||
f"Высокая исходящая связность: C_out={module.coupling_out} "
|
||
f"(порог {self.t.coupling_out_warn})"
|
||
)
|
||
|
||
# --- Входящая связность (fan-in) ---
|
||
if module.coupling_in > self.t.coupling_in_critical:
|
||
score += 2
|
||
reasons.append(
|
||
f"Критическая входящая связность: C_in={module.coupling_in} "
|
||
f"(порог {self.t.coupling_in_critical}) — нестабильный центральный узел"
|
||
)
|
||
elif module.coupling_in > self.t.coupling_in_warn:
|
||
score += 1
|
||
reasons.append(
|
||
f"Высокая входящая связность: C_in={module.coupling_in} "
|
||
f"(порог {self.t.coupling_in_warn})"
|
||
)
|
||
|
||
# --- Нестабильность (Instability, метрика Р. Мартина) ---
|
||
if module.instability > self.t.instability_critical:
|
||
score += 2
|
||
reasons.append(
|
||
f"Критическая нестабильность: I={module.instability:.2f} "
|
||
f"(порог {self.t.instability_critical})"
|
||
)
|
||
elif module.instability > self.t.instability_warn:
|
||
score += 1
|
||
reasons.append(
|
||
f"Высокая нестабильность: I={module.instability:.2f} "
|
||
f"(порог {self.t.instability_warn})"
|
||
)
|
||
|
||
# --- Циклические зависимости ---
|
||
if module.has_cycles:
|
||
score += 3
|
||
reasons.append("Участвует в циклических зависимостях — признак архитектурной деградации")
|
||
|
||
return score, reasons
|
||
|
||
# ------------------------------------------------------------------
|
||
# Преобразование очков в решения
|
||
# ------------------------------------------------------------------
|
||
|
||
@staticmethod
|
||
def _score_to_risk(score: int) -> RiskLevel:
|
||
"""Преобразует суммарный счёт в уровень риска."""
|
||
if score == 0:
|
||
return RiskLevel.LOW
|
||
if score <= 2:
|
||
return RiskLevel.MEDIUM
|
||
if score <= 5:
|
||
return RiskLevel.HIGH
|
||
return RiskLevel.CRITICAL
|
||
|
||
@staticmethod
|
||
def _risk_to_strategy(risk: RiskLevel) -> ModernizationStrategy:
|
||
"""
|
||
Определяет стратегию модернизации по уровню риска.
|
||
|
||
Логика основана на классификации Fowler (2018) и
|
||
Lehman & Belady (1985).
|
||
"""
|
||
return {
|
||
RiskLevel.LOW: ModernizationStrategy.KEEP,
|
||
RiskLevel.MEDIUM: ModernizationStrategy.REFACTOR,
|
||
RiskLevel.HIGH: ModernizationStrategy.REENGINEER,
|
||
RiskLevel.CRITICAL: ModernizationStrategy.REPLACE,
|
||
}[risk]
|
||
|
||
@staticmethod
|
||
def _compute_priority(score: int, module: ModuleMetrics) -> int:
|
||
"""
|
||
Приоритет в плане рефакторинга: меньше = выше приоритет.
|
||
Модули с циклами и высоким C_in получают наивысший приоритет
|
||
(они блокируют изменения во множестве других модулей).
|
||
"""
|
||
priority = 1000 - score * 100
|
||
|
||
# Модули с высоким fan-in важнее менять первыми —
|
||
# они наиболее болезненные для всей системы
|
||
if module.coupling_in > 0:
|
||
priority -= module.coupling_in * 10
|
||
|
||
if module.has_cycles:
|
||
priority -= 200
|
||
|
||
return max(priority, 1)
|
||
|
||
@staticmethod
|
||
def _system_risk(decisions: List[ModuleDecision]) -> RiskLevel:
|
||
"""Определяет системный уровень риска на основе распределения модулей."""
|
||
if not decisions:
|
||
return RiskLevel.LOW
|
||
|
||
total = len(decisions)
|
||
critical_ratio = sum(1 for d in decisions if d.risk_level == RiskLevel.CRITICAL) / total
|
||
high_ratio = sum(1 for d in decisions if d.risk_level in (RiskLevel.HIGH, RiskLevel.CRITICAL)) / total
|
||
|
||
if critical_ratio > 0.3:
|
||
return RiskLevel.CRITICAL
|
||
if high_ratio > 0.5:
|
||
return RiskLevel.HIGH
|
||
if high_ratio > 0.2:
|
||
return RiskLevel.MEDIUM
|
||
return RiskLevel.LOW
|