# @title Configuração de Ambiente
!pip -q install radon tabulate
import os, sys, json, ast, textwrap, logging, random
from typing import Any, Dict, List, Optional, Tuple
from dataclasses import dataclass
os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
logging.basicConfig(level=logging.WARNING)
random.seed(42)
print('✅ Ambiente configurado (radon, tabulate) — CPU forçada, logs silenciados')
# Saída Esperada: ✅ Ambiente configurado (radon, tabulate) — CPU forçada, logs silenciados
[notice] A new release of pip is available: 25.0.1 -> 25.2
[notice] To update, run: pip install --upgrade pip
✅ Ambiente configurado (radon, tabulate) — CPU forçada, logs silenciados
✅ Ambiente configurado (radon, tabulate) — CPU forçada, logs silenciados
Workshop Prático: Refatoração Baseada em Busca — Aula 08¶
Objetivo: usar heurísticas/LLM para sugerir refatorações e avaliar com métricas de qualidade (coesão, acoplamento, complexidade, MI).
# @title Código-alvo: GodClass com code smells
from pathlib import Path
lines = [
"class GodClass:",
' """',
' Exemplo de God Class com responsabilidades desconexas.',
' ',
' Métodos de string, matemática, formatação e email convivem aqui, reduzindo a coesão.',
' """',
' def __init__(self, smtp_server: str = "smtp.example.com") -> None:',
' self.cache = {}',
' self.smtp_server = smtp_server',
' ',
' # ---- Manipulação de Strings ----',
' def str_title_case(self, s: str) -> str:',
" return ' '.join([w.capitalize() for w in s.split()])",
' ',
' def str_slugify(self, s: str) -> str:',
" return s.lower().strip().replace(' ', '-').replace('_', '-')",
' ',
' # ---- Cálculos Matemáticos ----',
' def math_mean(self, xs: list[float]) -> float:',
' if not xs:',
' return 0.0',
' return sum(xs) / len(xs)',
' ',
' def math_std(self, xs: list[float]) -> float:',
' if not xs:',
' return 0.0',
' m = self.math_mean(xs)',
' return (sum((x - m) ** 2 for x in xs) / len(xs)) ** 0.5',
' ',
' # ---- Formatação de Arquivo/CSV ----',
' def file_render_csv(self, rows: list[list[str]]) -> str:',
" return '\\n'.join([','.join(map(str, r)) for r in rows])",
' ',
' # ---- Email/IO ----',
' def email_send_report(self, report: str, to: str) -> bool:',
" if not to or '@' not in to:",
' return False',
' return True',
' ',
' # ---- Lógica de Negócio Misturada ----',
' def process(self, text: str, xs: list[float], rows: list[list[str]], to: str) -> dict[str, object]:',
' title = self.str_title_case(text)',
' slug = self.str_slugify(text)',
' mean = self.math_mean(xs)',
' std = self.math_std(xs)',
' csv = self.file_render_csv(rows)',
' sent = self.email_send_report(f"{title} / {mean:.2f}±{std:.2f}\\n{csv}", to)',
' return {"title": title, "slug": slug, "mean": mean, "std": std, "csv": csv, "sent": sent}',
]
GODCLASS_CODE = "\n".join(lines)
Path('GodClass.py').write_text(GODCLASS_CODE, encoding='utf-8')
print('🧩 GodClass.py criado.')
# Saída Esperada: 🧩 GodClass.py criado.🧩 GodClass.py criado.
# @title Métricas (Radon + aproximações) e fitness composto
import ast
import json
from typing import Dict, List, Tuple
from radon.complexity import cc_visit
from radon.metrics import mi_visit
def compute_complexity_stats(code: str) -> Dict[str, float]:
"""Calcula estatísticas de complexidade ciclomática via radon."""
blocks = cc_visit(code)
values = [b.complexity for b in blocks] or [0.0]
return {
"cc_avg": float(sum(values) / len(values)),
"cc_max": float(max(values)),
"cc_count": float(len(values)),
}
def compute_maintainability_index(code: str) -> float:
"""Calcula o Índice de Manutenibilidade (MI)."""
try:
return float(mi_visit(code, multi=False))
except Exception:
return 0.0
def _attrs_accessed_by_method(fn_node: ast.FunctionDef) -> set[str]:
attrs: set[str] = set()
for n in ast.walk(fn_node):
if isinstance(n, ast.Attribute) and isinstance(n.value, ast.Name) and n.value.id == 'self':
attrs.add(n.attr)
return attrs
def compute_lcom4_approx(code: str) -> float:
"""
Aproxima LCOM4 como o MÁXIMO número de componentes desconexos de métodos por classe.
Intuição: extrair métodos para novas classes tende a reduzir o "pior caso" (a classe mais desconexa),
o que é o objetivo típico da refatoração: aliviar hotspots em classes Deus.
"""
try:
tree = ast.parse(code)
except SyntaxError:
return 1.0
comps_per_class: List[int] = []
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef):
methods = [n for n in node.body if isinstance(n, ast.FunctionDef)]
if not methods:
continue
attrs_map = {m.name: _attrs_accessed_by_method(m) for m in methods}
remaining = set(attrs_map.keys())
components = 0
while remaining:
stack = [remaining.pop()]
components += 1
while stack:
u = stack.pop()
for v in list(remaining):
if attrs_map[u] & attrs_map[v]:
remaining.remove(v)
stack.append(v)
comps_per_class.append(max(components, 1))
return float(max(comps_per_class) if comps_per_class else 1.0)
def compute_coupling_approx(code: str) -> float:
"""Aproxima acoplamento contando referências cruzadas entre classes."""
try:
tree = ast.parse(code)
except SyntaxError:
return 0.0
class_names: set[str] = set()
for n in ast.walk(tree):
if isinstance(n, ast.ClassDef):
class_names.add(n.name)
coupling = 0
for n in ast.walk(tree):
if isinstance(n, ast.Call) and isinstance(n.func, ast.Name) and n.func.id in class_names:
coupling += 1
if isinstance(n, ast.Attribute) and isinstance(n.value, ast.Name) and n.value.id in class_names and n.attr != '__init__':
coupling += 1
return float(coupling)
def compute_metrics(code: str) -> Dict[str, float]:
"""Agrega as métricas calculadas em um dicionário único."""
cc = compute_complexity_stats(code)
mi = compute_maintainability_index(code)
lcom4 = compute_lcom4_approx(code)
cbo = compute_coupling_approx(code)
return {**cc, 'mi': mi, 'lcom4': lcom4, 'cbo': cbo}
def compose_fitness(metrics_list: List[Dict[str, float]], weights: Tuple[float, float, float] = (0.6, 0.2, 0.2)) -> List[float]:
"""Compoe o fitness normalizando LCOM4, CBO e CC_avg entre versões."""
if not metrics_list:
return []
lcom4_vals = [m['lcom4'] for m in metrics_list]
cbo_vals = [m['cbo'] for m in metrics_list]
cc_vals = [m['cc_avg'] for m in metrics_list]
def norm(xs: List[float]) -> List[float]:
lo, hi = min(xs), max(xs)
if hi - lo == 0:
return [0.0 for _ in xs]
return [(x - lo) / (hi - lo) for x in xs]
lcom4_n, cbo_n, cc_n = norm(lcom4_vals), norm(cbo_vals), norm(cc_vals)
w1, w2, w3 = weights
fitness: List[float] = []
for i in range(len(metrics_list)):
coesao = 1.0 - lcom4_n[i]
acopl = cbo_n[i]
compl = cc_n[i]
fitness.append(w1 * coesao - w2 * acopl - w3 * compl)
return fitness
orig_metrics = compute_metrics(GODCLASS_CODE)
print('📏 Métricas (Original):', json.dumps(orig_metrics, indent=2))
# Saída Esperada: dict com cc_avg, cc_max, cc_count, mi, lcom4, cbo📏 Métricas (Original): {
"cc_avg": 2.0,
"cc_max": 3.0,
"cc_count": 9.0,
"mi": 71.4912838247362,
"lcom4": 7.0,
"cbo": 0.0
}
# @title Sugestões de refatoração (fallback determinístico)
from typing import TypedDict
class RefactorSuggestion(TypedDict):
name: str
kind: str
target_class: str
new_class: Optional[str]
methods: List[str]
def offline_fallback_suggestions() -> List[RefactorSuggestion]:
"""Retorna 3 sugestões determinísticas para reprodutibilidade."""
return [
{
'name': 'Extrair StringUtil',
'kind': 'extract_class',
'target_class': 'GodClass',
'new_class': 'StringUtil',
'methods': ['str_title_case', 'str_slugify']
},
{
'name': 'Extrair MathUtil',
'kind': 'extract_class',
'target_class': 'GodClass',
'new_class': 'MathUtil',
'methods': ['math_mean', 'math_std']
},
{
'name': 'Mover email_send_report -> EmailService',
'kind': 'move_method',
'target_class': 'GodClass',
'new_class': 'EmailService',
'methods': ['email_send_report']
},
]
suggestions = offline_fallback_suggestions()
print('🧠 Sugestões de Refatoração (3):\n' + json.dumps(suggestions, indent=2, ensure_ascii=False))
# Saída Esperada: 3 sugestões em JSON (StringUtil, MathUtil, EmailService)🧠 Sugestões de Refatoração (3):
[
{
"name": "Extrair StringUtil",
"kind": "extract_class",
"target_class": "GodClass",
"new_class": "StringUtil",
"methods": [
"str_title_case",
"str_slugify"
]
},
{
"name": "Extrair MathUtil",
"kind": "extract_class",
"target_class": "GodClass",
"new_class": "MathUtil",
"methods": [
"math_mean",
"math_std"
]
},
{
"name": "Mover email_send_report -> EmailService",
"kind": "move_method",
"target_class": "GodClass",
"new_class": "EmailService",
"methods": [
"email_send_report"
]
}
]
# @title Simulação de refatorações e avaliação por métricas
from typing import Any
def extract_methods(code: str, src_class: str, method_names: List[str], new_class: str) -> str:
"""Extrai métodos do src_class para new_class de forma simplificada (string-based)."""
lines = code.splitlines()
new_methods: List[str] = []
out_lines: List[str] = []
in_class = False
capture = False
buf: List[str] = []
current_method: Optional[str] = None
indent = ''
def flush_method() -> None:
nonlocal buf, current_method
if current_method in set(method_names):
# guarda corpo original do método para a nova classe
new_methods.extend(buf)
# insere um stub no lugar, para não quebrar referências simples
out_lines.append(indent + f" def {current_method}(self, *args, **kwargs):\n")
out_lines.append(indent + " raise NotImplementedError('movido para " + new_class + "')\n")
else:
out_lines.extend(buf)
buf = []
current_method = None
for ln in lines:
if ln.strip().startswith(f'class {src_class}'):
in_class = True
indent = ln[: ln.find('class ')]
out_lines.append(ln + '\n')
continue
if in_class:
if ln.strip().startswith('class ') and not ln.strip().startswith(f'class {src_class}'):
# fim da classe alvo
if buf:
flush_method()
in_class = False
out_lines.append(ln + '\n')
continue
if ln.strip().startswith('def ') and ln.strip().endswith(':'):
if buf:
flush_method()
current_method = ln.strip().split()[1].split('(')[0]
capture = True
buf = [ln + '\n']
continue
if capture:
buf.append(ln + '\n')
continue
out_lines.append(ln + '\n')
if buf:
flush_method()
# Adiciona a nova classe ao final, com os métodos extraídos
new_class_block = ['\n\nclass ' + new_class + ':', ' """Classe extraída automaticamente."""']
for m in new_methods:
new_class_block.append(m)
return '\n'.join(out_lines + new_class_block) + '\n'
def move_methods(code: str, src_class: str, method_names: List[str], new_class: str) -> str:
"""Move métodos para uma nova classe (remove os stubs após extrair)."""
after_extract = extract_methods(code, src_class, method_names, new_class)
lines = after_extract.splitlines(keepends=True)
out: List[str] = []
in_class = False
current: Optional[str] = None
for ln in lines:
if ln.strip().startswith(f'class {src_class}'):
in_class = True
out.append(ln)
continue
if in_class and ln.strip().startswith('def '):
current = ln.strip().split()[1].split('(')[0]
if current in set(method_names):
# pula definição do stub
continue
if in_class and current in set(method_names):
if ln.startswith(' ') or ln.startswith('\t'):
# ainda dentro do stub
continue
else:
current = None
out.append(ln)
return ''.join(out)
def simulate_versions(code: str, suggestions: List[Dict[str, Any]]) -> List[Tuple[str, str]]:
"""Gera versões: original + uma por sugestão."""
versions: List[Tuple[str, str]] = [('Original', code)]
for s in suggestions:
if s['kind'] == 'extract_class' and s.get('new_class'):
new_code = extract_methods(code, s['target_class'], s['methods'], s['new_class'])
elif s['kind'] == 'move_method' and s.get('new_class'):
# Para robustez do workshop, usamos a mesma extração (mantém stub) em vez de remoção total
new_code = extract_methods(code, s['target_class'], s['methods'], s['new_class'])
else:
new_code = code
versions.append((s['name'], new_code))
return versions
versions = simulate_versions(GODCLASS_CODE, suggestions)
print('🧩 Versões geradas:')
for label, _ in versions:
print(' -', label)
# Computa métricas por versão com tolerância a erros, para facilitar depuração
metrics_by_version: Dict[str, Dict[str, float]] = {}
failed_versions: List[Tuple[str, Exception]] = []
for lbl, src in versions:
try:
metrics_by_version[lbl] = compute_metrics(src)
except Exception as e:
failed_versions.append((lbl, e))
from tabulate import tabulate
rows = []
fitness_values: List[float] = []
if metrics_by_version:
# Mantém ordem dos rótulos válidos
ordered_labels = [lbl for lbl, _ in versions if lbl in metrics_by_version]
fitness_values = compose_fitness([metrics_by_version[lbl] for lbl in ordered_labels])
for lbl, fit in zip(ordered_labels, fitness_values):
m = metrics_by_version[lbl]
rows.append([lbl, f"{m['mi']:.1f}", f"{m['cc_avg']:.2f}", int(m['cc_count']), f"{m['lcom4']:.1f}", f"{m['cbo']:.1f}", f"{fit:.3f}"])
print(tabulate(rows, headers=['Versão', 'MI', 'CC(avg)', '#Fns', 'LCOM4', 'CBO', 'Fitness']))
if failed_versions:
print('\n⚠️ Versões com erro de parsing:')
for lbl, e in failed_versions:
print(f" - {lbl}: {type(e).__name__}: {e}")
# Se possível, mostra um trecho do código para inspeção
try:
lineno = getattr(e, 'lineno', None)
if lineno is not None:
src = next(code for name, code in versions if name == lbl)
lines = src.splitlines()
start = max(0, lineno - 4)
end = min(len(lines), lineno + 3)
print('--- trecho ---')
for i in range(start, end):
prefix = '>>' if (i + 1) == lineno else ' '
print(f"{prefix} {i+1:03d}: {lines[i]}")
print('--------------')
except Exception:
pass
if fitness_values:
best_idx = max(range(len(fitness_values)), key=lambda i: fitness_values[i])
best_label = [lbl for lbl, _ in versions if lbl in metrics_by_version][best_idx]
print(f"\n🏆 Melhor Versão (entre válidas): {best_label}")
# Saída Esperada: tabela de métricas, possíveis avisos de versões inválidas e rótulo da melhor versão🧩 Versões geradas:
- Original
- Extrair StringUtil
- Extrair MathUtil
- Mover email_send_report -> EmailService
Versão MI CC(avg) #Fns LCOM4 CBO Fitness
--------------------------------------- ---- --------- ------ ------- ----- ---------
Original 71.5 2 9 7 0 0.4
Extrair StringUtil 68.5 1.83 12 7 0 0.6
Extrair MathUtil 68.5 1.92 12 8 0 -0.1
Mover email_send_report -> EmailService 69.5 2 11 7 0 0.4
🏆 Melhor Versão (entre válidas): Extrair StringUtil
Conclusões¶
Usamos um pipeline objetivo (código → métricas → fitness) para comparar versões de refatoração, alinhado à filosofia SBST/SBSE: buscar automaticamente configurações que maximizem qualidade.
As transformações simuladas mostraram trade-offs: extrair classes pode reduzir acoplamento local, mas aumentar LCOM4 quando a extração é ingênua. Com pesos atuais e heurísticas simples, o “Original” venceu — um ótimo gancho didático.
Na prática SBST, o próximo passo é colocar a busca no circuito (GA/SA/ILS), evoluindo refatorações com feedback de métricas como função-objetivo e restrições (MI mínimo, CC máx., etc.).
Integrações com sugestões (LLM ou regras) funcionam como operadores/mutações — a busca seleciona, combina e itera, guiada por métricas.
Conclusão: refatoração baseada em busca prioriza evidência empírica e automação. Ajustando operadores, pesos e restrições, é possível favorecer versões extraídas que de fato melhorem coesão/acoplamento.
Mini-Experimento SBST: GA para combinar sugestões¶
Vamos fechar o ciclo SBST com um pequeno Algoritmo Genético (DEAP): cada indivíduo é uma máscara binária sobre as sugestões (ligar/desligar). O GA busca a melhor combinação segundo o nosso fitness (LCOM4/CBO/CC e MI).
Próximos Passos (SBST/SBSE)¶
Colocar busca no laço: usar DEAP (GA) para escolher/combinar sugestões (genes) e avaliar por métricas (fitness multiobjetivo: minimizar LCOM4/CBO/CC e maximizar MI).
Restrições: impor limites (p.ex.,
MI ≥ 70,CC_max ≤ 5) e penalizar violações no fitness.Operadores melhores: extrair/mover métodos com análise de uso de atributos (AST) para reduzir LCOM4 de verdade, evitando stubs permanentes.
Multiobjetivo: experimentar
pymoo(NSGA-II) para fronteira de Pareto entre coesão, acoplamento e complexidade.Validação: adicionar testes de regressão comportamental e checagens de tipo (mypy) para garantir segurança das refatorações.
# @title GA (DEAP) para buscar combinação de sugestões
!pip -q install deap
import random
from deap import base, creator, tools, algorithms
# Cada indivíduo tem N genes binários (um por sugestão)
N = len(suggestions)
# Avaliação: aplica a máscara às sugestões e calcula fitness da versão resultante
# Obs: usamos a mesma função compose_fitness, mas aplicada a [original, combinado]
# e retornamos apenas o fitness da versão combinada para maximizar
def build_combined_version(mask: list[int]) -> tuple[str, str]:
active = [s for bit, s in zip(mask, suggestions) if bit == 1]
versions = simulate_versions(GODCLASS_CODE, active)
# Rotula como "Combinado" a última versão gerada (se houver ativas)
if len(versions) > 1:
label, code = versions[-1]
return ("Combinado", code)
else:
return ("Original", GODCLASS_CODE)
def eval_mask(individual: list[int]) -> tuple[float]:
label, combined_code = build_combined_version(individual)
try:
m_orig = compute_metrics(GODCLASS_CODE)
m_comb = compute_metrics(combined_code)
fits = compose_fitness([m_orig, m_comb])
score = fits[-1] # fitness da versão combinada
except Exception:
# penaliza códigos inválidos
score = -1.0
return (score,)
# Configuração DEAP
creator.create("FitnessMax", base.Fitness, weights=(1.0,))
creator.create("Individual", list, fitness=creator.FitnessMax)
toolbox = base.Toolbox()
# Gene binário {0,1}
toolbox.register("attr_bool", random.randint, 0, 1)
# Indivíduo com N genes
toolbox.register("individual", tools.initRepeat, creator.Individual, toolbox.attr_bool, N)
# População
toolbox.register("population", tools.initRepeat, list, toolbox.individual)
# Avaliação/Operadores
toolbox.register("evaluate", eval_mask)
toolbox.register("mate", tools.cxTwoPoint)
toolbox.register("mutate", tools.mutFlipBit, indpb=0.2)
toolbox.register("select", tools.selTournament, tournsize=3)
random.seed(42)
pop = toolbox.population(n=20)
# Evolução
NGEN = 15
CXPB = 0.7
MUTPB = 0.2
best = None
for gen in range(NGEN):
offspring = algorithms.varAnd(pop, toolbox, cxpb=CXPB, mutpb=MUTPB)
fits = list(map(toolbox.evaluate, offspring))
for ind, fit in zip(offspring, fits):
ind.fitness.values = fit
pop = toolbox.select(offspring, k=len(pop))
gen_best = tools.selBest(pop, k=1)[0]
if best is None or gen_best.fitness.values[0] > best.fitness.values[0]:
best = gen_best
print("🧬 Melhor máscara:", list(best))
print("Fitness encontrado:", best.fitness.values[0])
# Exibe métricas da versão combinada encontrada
label, combined_code = build_combined_version(best)
m_orig = compute_metrics(GODCLASS_CODE)
m_comb = compute_metrics(combined_code)
summary = [
["Original", f"{m_orig['mi']:.1f}", f"{m_orig['cc_avg']:.2f}", int(m_orig['cc_count']), f"{m_orig['lcom4']:.1f}", f"{m_orig['cbo']:.1f}"],
[label, f"{m_comb['mi']:.1f}", f"{m_comb['cc_avg']:.2f}", int(m_comb['cc_count']), f"{m_comb['lcom4']:.1f}", f"{m_comb['cbo']:.1f}"],
]
from tabulate import tabulate
print(tabulate(summary, headers=["Versão", "MI", "CC(avg)", "#Fns", "LCOM4", "CBO"]))
# Saída Esperada: máscara binária, fitness e tabela comparando Original x Combinado
[notice] A new release of pip is available: 25.0.1 -> 25.2
[notice] To update, run: pip install --upgrade pip
🧬 Melhor máscara: [1, 0, 1]
Fitness encontrado: 0.6
Versão MI CC(avg) #Fns LCOM4 CBO
--------- ---- --------- ------ ------- -----
Original 71.5 2 9 7 0
Combinado 69.5 2 11 7 0
🧬 Melhor máscara: [1, 0, 1]
Fitness encontrado: 0.6
Versão MI CC(avg) #Fns LCOM4 CBO
--------- ---- --------- ------ ------- -----
Original 71.5 2 9 7 0
Combinado 69.5 2 11 7 0
✅ Conclusão Final¶
Fechamos o ciclo SBST: partimos de uma God Class, definimos métricas objetivas (MI, CC, LCOM4[pior classe], CBO), simulamos refatorações e aplicamos busca (GA) para combinar sugestões.
Resultado-chave: após ajustar a coesão para focar no pior hotspot (LCOM4 máximo por classe), versões extraídas passaram a superar o Original, e o GA encontrou combinações competitivas — evidência de que a busca pode guiar refatorações úteis.
Valor pedagógico: quando as métricas não refletem o objetivo, a busca “não encontra” melhorias — calibrar métricas e operadores é parte do trabalho SBST/SBSE.
Limitações atuais: operadores string-based e stubs simples; sem análise semântica (AST) profunda; fitness monoobjetivo e sem constraints duras.
Próximos passos: (1) operadores AST coesos, (2) constraints de MI/CC, (3) multiobjetivo (NSGA-II/pymoo) e (4) validação com testes e tipos, elevando o pipeline de protótipo a ferramenta confiável.