Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Workshop Prático: Refatoração Baseada em Busca — Aula 08

# @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.