📚 Material de Apoio - Aula 03

Refatoração Segura em Java

Desenvolvimento de Sistemas II
Prof. Ricardo Pires
3º Técnico DS


🎯 Quick Reference: Técnicas de Refatoração

🔧 Extract Method

// ANTES
public void longMethod() {
    // Validação (10 linhas)
    if (data == null) return;
    if (data.isEmpty()) return;
    // ... mais validações
 
    // Processamento (15 linhas)
    for (Item item : data) {
        // lógica complexa
    }
 
    // Formatação (8 linhas)
    String result = format();
    // ... formatação
}
 
// DEPOIS
public void processData() {
    if (!isValid(data)) return;
 
    List<ProcessedItem> processed = processItems(data);
    String result = formatResult(processed);
 
    saveResult(result);
}
 
private boolean isValid(Data data) {
    return data != null && !data.isEmpty();
}
 
private List<ProcessedItem> processItems(Data data) {
    // lógica extraída
}
 
private String formatResult(List<ProcessedItem> items) {
    // formatação extraída
}

🌟 Decompose Conditional

// ANTES - Condicionais Aninhadas
if (user != null) {
    if (user.isActive()) {
        if (user.hasPermission("READ")) {
            if (document != null) {
                if (document.isPublished()) {
                    return document.getContent();
                }
            }
        }
    }
}
return null;
 
// DEPOIS - Guard Clauses
public String getDocumentContent(User user, Document document) {
    if (!isUserValid(user)) return null;
    if (!isDocumentAccessible(user, document)) return null;
 
    return document.getContent();
}
 
private boolean isUserValid(User user) {
    return user != null &&
           user.isActive() &&
           user.hasPermission("READ");
}
 
private boolean isDocumentAccessible(User user, Document document) {
    return document != null &&
           document.isPublished();
}

📊 Code Smells: Catálogo Completo

🚩 1. Método Longo (Long Method)

Sintomas:

  • Métodos com 20+ linhas
  • Múltiplas responsabilidades
  • Dificulta compreensão e teste

Solução: Extract Method

// Quebrar em métodos menores e focados
public void processOrder(Order order) {
    validateOrder(order);
    calculateTotal(order);
    applyDiscounts(order);
    saveOrder(order);
    sendNotification(order);
}

🚩 2. Condicionais Complexas (Complex Conditional)

Sintomas:

  • Múltiplos níveis de aninhamento
  • Condições longas e difíceis de ler
  • Lógica boolean complexa

Solução: Decompose Conditional + Guard Clauses

// Em vez de condições aninhadas, usar early returns
public boolean canAccess(User user, Resource resource) {
    if (user == null) return false;
    if (!user.isActive()) return false;
    if (resource == null) return false;
    if (!resource.isPublic() && !user.hasRole("ADMIN")) return false;
 
    return true;
}

🚩 3. Código Duplicado (Duplicate Code)

Sintomas:

  • Mesmo código em múltiplos lugares
  • Lógica similar com pequenas variações
  • Bug fixes que precisam ser aplicados em vários locais

Solução: Extract Method + Parameterização

// Extrair lógica comum
private double calculateDiscount(Customer customer, double amount, DiscountType type) {
    if (!customer.isEligibleForDiscount()) return 0;
 
    return switch (type) {
        case VIP -> amount * 0.15;
        case REGULAR -> amount * 0.05;
        case SEASONAL -> amount * getSeasonalRate();
    };
}

🚩 4. Lista de Parâmetros Longa (Long Parameter List)

Sintomas:

  • Métodos com 4+ parâmetros
  • Parâmetros frequentemente passados juntos
  • Dificulta chamadas do método

Solução: Parameter Object

// ANTES
public void createUser(String name, String email, int age,
                      String address, String phone, String department) {
    // ...
}
 
// DEPOIS
public void createUser(UserData userData) {
    // ...
}
 
public class UserData {
    private String name, email, address, phone, department;
    private int age;
    // getters/setters
}

🚩 5. Classe Grande (Large Class)

Sintomas:

  • Classes com 500+ linhas
  • Muitas responsabilidades
  • Dificulta navegação e manutenção

Solução: Extract Class

// Separar responsabilidades
public class UserService {
    public void createUser(UserData data) { }
    public void updateUser(User user) { }
}
 
public class UserValidator {
    public ValidationResult validate(UserData data) { }
}
 
public class UserNotifier {
    public void sendWelcomeEmail(User user) { }
}

🚩 6. Nomes Confusos (Unclear Names)

Sintomas:

  • Variáveis com nomes genéricos (data, info, temp)
  • Métodos que não expressam intenção
  • Nomes que mentem sobre funcionalidade

Solução: Rename Method/Variable

// ANTES
public boolean check(User u, double v, int t) {
    return u.getCredit() > v && u.getDays() > t;
}
 
// DEPOIS
public boolean hasEnoughCreditAndExperience(User user,
                                           double requiredCredit,
                                           int minimumDays) {
    return user.getCredit() > requiredCredit &&
           user.getDays() > minimumDays;
}

🚩 7. Comentários Excessivos (Comments)

Sintomas:

  • Comentários explicando o que código faz
  • Comentários desatualizados
  • Código que precisa comentário para ser entendido

Solução: Self-Documenting Code

// ANTES
// Desconto de 10% se cliente VIP com mais de 100 compras
if (c.isVip() && c.getPurchases() > 100) {
    total = total * 0.9; // aplica desconto
}
 
// DEPOIS
if (isEligibleForVipDiscount(customer)) {
    total = applyVipDiscount(total);
}
 
private boolean isEligibleForVipDiscount(Customer customer) {
    return customer.isVip() && customer.getPurchases() > 100;
}
 
private double applyVipDiscount(double total) {
    return total * 0.9;
}

🛠️ Ferramentas de Refatoração

💡 IntelliJ IDEA - Shortcuts Essenciais

RefatoraçãoAtalhoUso
Extract MethodCtrl+Alt+MExtrair código selecionado para método
Extract VariableCtrl+Alt+VCriar variável para expressão
Extract ConstantCtrl+Alt+CCriar constante para valor literal
RenameShift+F6Renomear símbolo em todo projeto
InlineCtrl+Alt+NRemover método/variável desnecessária
Change SignatureCtrl+F6Alterar parâmetros de método
MoveF6Mover método entre classes

🎯 Eclipse - Atalhos Principais

RefatoraçãoAtalhoUso
Extract MethodAlt+Shift+MExtrair método
Extract Local VariableAlt+Shift+LCriar variável local
RenameAlt+Shift+RRenomear elemento
Quick FixCtrl+1Sugestões automáticas

📊 Métricas de Qualidade

Complexidade Ciclomática

// Complexidade = 1 (caminho linear)
public String getGrade(int score) {
    return score >= 60 ? "Pass" : "Fail";
}
 
// Complexidade = 4 (4 caminhos possíveis)
public String getGrade(int score) {
    if (score >= 90) return "A";      // +1
    else if (score >= 80) return "B"; // +1
    else if (score >= 70) return "C"; // +1
    else return "F";                  // +1 (base)
}

Target: Complexidade ≤ 5 por método

Linhas de Código por Método

// BOM: Método focado (8 linhas)
public User createUser(String email, String password) {
    validateEmail(email);
    validatePassword(password);
 
    User user = new User(email, encryptPassword(password));
    userRepository.save(user);
 
    return user;
}
 
// RUIM: Método longo (40+ linhas)
public User createUserWithValidationAndEmailAndLogging(...) {
    // 40+ linhas fazendo várias coisas
}

Target: ≤ 20 linhas por método


🧪 Estratégias de Teste para Refatoração

🛡️ Testes de Caracterização

/**
 * Testes que capturam comportamento atual ANTES da refatoração
 * Garantem que refatoração preserva funcionalidade
 */
@Test
@DisplayName("Captura comportamento atual para refatoração segura")
void testeCaracterizacao() {
    // Arrange - setup idêntico ao sistema em produção
    ProcessadorPedido processor = new ProcessadorPedido();
    Pedido pedido = criarPedidoCompleto();
 
    // Act - executar método que será refatorado
    String resultado = processor.processar(pedido);
 
    // Assert - documentar exatamente o que acontece hoje
    assertEquals("Pedido processado. Total: R$ 150.00", resultado);
    // Este teste DEFINE o comportamento que deve ser preservado
}

🎯 Testes Parametrizados para Cobertura

@ParameterizedTest
@CsvSource({
    "100, REGULAR, false, 100.0",
    "100, VIP, true, 85.0",
    "50, REGULAR, false, 50.0",
    "1000, VIP, true, 850.0"
})
@DisplayName("Preserva cálculo de desconto após refatoração")
void testeCalculoDesconto(double valor, CustomerType tipo,
                         boolean temDesconto, double esperado) {
 
    Customer customer = new Customer(tipo);
    double resultado = calculadoraDesconto.calcular(customer, valor);
 
    assertEquals(esperado, resultado, 0.01);
}

🔄 Testes de Mutação

@Test
@DisplayName("Validação que testes capturam mudanças essenciais")
void testeMutacao() {
    // Simula mudanças pequenas no código para verificar se testes quebram
 
    // Se trocar > por >= aqui, teste DEVE quebrar
    assertTrue(validador.isElegivel(customer, 100.01)); // deve passar
    assertFalse(validador.isElegivel(customer, 100.0)); // deve falhar
 
    // Se este teste não quebrar quando lógica muda,
    // o teste não está validando o comportamento essencial
}

⚡ Testes de Performance

@Test
@DisplayName("Refatoração não degradou performance significativamente")
void testePerformance() {
    // Medir antes da refatoração
    long inicio = System.nanoTime();
 
    for (int i = 0; i < 1000; i++) {
        processor.processar(criarPedidoComplexo());
    }
 
    long tempo = System.nanoTime() - inicio;
 
    // Aceitar até 20% de degradação
    assertTrue(tempo < BASELINE_TEMPO * 1.2,
              "Performance degradou mais que 20%");
}

📚 Referências e Leitura Adicional

📖 Livros Fundamentais

1. Refactoring: Improving the Design of Existing Code

Autor: Martin Fowler
Por que ler: Catálogo completo de técnicas de refatoração

📌 Capítulos essenciais para esta aula:
- Chapter 1: Refactoring, a First Example
- Chapter 3: Bad Smells in Code
- Chapter 6: Extract Method
- Chapter 9: Simplifying Conditional Expressions

2. Clean Code: A Handbook of Agile Software Craftsmanship

Autor: Robert C. Martin
Por que ler: Princípios de código limpo e legível

📌 Capítulos relevantes:
- Chapter 3: Functions (Extract Method)
- Chapter 17: Smells and Heuristics
- Chapter 5: Formatting

3. Working Effectively with Legacy Code

Autor: Michael Feathers
Por que ler: Técnicas para refatorar código sem testes

📌 Técnicas importantes:
- Characterization Tests
- Seam identification
- Breaking dependencies

🌐 Recursos Online

Sites Essenciais

  • Refactoring.guru

    • Explicações visuais de cada técnica
    • Exemplos em múltiplas linguagens
    • Exercícios interativos
  • Refactoring.com

    • Site oficial do Martin Fowler
    • Catálogo completo de refatorações
    • Artigos e atualizações
  • SourceMaking

    • Design patterns + refactoring
    • Exemplos práticos
    • Code smells ilustrados

Videos e Tutoriais

🎥 YouTube Channels:
- "Code Aesthetic" - Refactoring in practice
- "Continuous Delivery" - Legacy code management
- "Uncle Bob Martin" - Clean code principles

🎯 Specific videos:
- "Refactoring: Extract Method" - IntelliJ Tutorial
- "The Art of Readable Code" - Google Tech Talk
- "Therapeutic Refactoring" - Katrina Owen

🛠️ Ferramentas de Apoio

Análise Estática

<!-- SonarQube Maven Plugin -->
<plugin>
    <groupId>org.sonarsource.scanner.maven</groupId>
    <artifactId>sonar-maven-plugin</artifactId>
    <version>3.9.1.2184</version>
</plugin>
 
<!-- PMD Plugin -->
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-pmd-plugin</artifactId>
    <version>3.17.0</version>
    <configuration>
        <rulesets>
            <ruleset>/rulesets/java/design.xml</ruleset>
            <ruleset>/rulesets/java/controversial.xml</ruleset>
        </rulesets>
    </configuration>
</plugin>

Cobertura de Testes

<!-- JaCoCo Coverage Plugin -->
<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.7</version>
    <executions>
        <execution>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>report</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Métricas de Código

<!-- Metrics Maven Plugin -->
<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>javancss-maven-plugin</artifactId>
    <version>2.1</version>
    <configuration>
        <failOnViolation>true</failOnViolation>
        <ccnLimit>5</ccnLimit>
        <ncssLimit>50</ncssLimit>
    </configuration>
</plugin>

💡 Dicas Avançadas

🎯 Quando NÃO Refatorar

❌ Situações de Alto Risco

// 1. Código sem testes adequados
public criticalPaymentMethod() {
    // Sem testes = refatoração perigosa
    // PRIMEIRO: escrever testes
    // DEPOIS: refatorar
}
 
// 2. Sistema instável
if (productionErrors.count() > 5) {
    // FOQUE: estabilizar primeiro
    // Refatoração depois da estabilidade
}
 
// 3. Deadline crítico
if (deploymentIn < 2.days()) {
    // EVITE: mudanças desnecessárias
    // Refatore na próxima sprint
}

⚖️ Trade-offs de Refatoração

// BENEFÍCIO vs. CUSTO
 
// ALTO BENEFÍCIO (refatorar!):
// - Código alterado frequentemente
// - Time crescendo (novos devs)
// - Bugs recorrentes na área
 
// BAIXO BENEFÍCIO (evitar):
// - Código legacy estável
// - Funcionalidade será removida
// - Time pequeno e experiente

🔄 Refatoração Incremental

📅 Estratégia “Boy Scout Rule”

// A cada alteração, deixe código um pouco melhor
 
// ANTES de implementar feature:
git commit -m "Refactor: extract validation method"
 
// DURANTE implementação:
git commit -m "Feature: add new validation rule"
 
// RESULTADO: melhoria contínua sem esforço extra

🎯 Refatoração por Sprints

Sprint N:   Extract Methods
Sprint N+1: Decompose Conditionals
Sprint N+2: Extract Classes
Sprint N+3: Introduce Patterns

// Pequenas melhorias constantes > grandes refatorações esporádicas

👥 Refatoração em Equipe

📋 Code Review Checklist

□ Comportamento preservado (testes passando)?
□ Nomes de métodos expressivos?
□ Single Responsibility mantida?
□ Complexidade ciclomática melhorou?
□ Duplicação eliminada?
□ Performance não degradou?
□ Documentação atualizada se necessário?

🏆 Métricas de Team

// Tracking de melhoria contínua
public class RefactoringMetrics {
 
    // Antes: 50 métodos com 20+ linhas
    // Meta Sprint 1: 40 métodos com 20+ linhas
    // Meta Sprint 2: 30 métodos com 20+ linhas
 
    @Scheduled(weekly)
    public void trackProgress() {
        int longMethods = countMethodsOver20Lines();
        double improvement = (baseline - current) / baseline * 100;
 
        slackNotify("Refactoring progress: " + improvement + "% improvement");
    }
}

🎯 Exercícios Complementares

🏋️ Exercício A: Code Smell Detection

// Analise este código e identifique TODOS os code smells
public class OrderManager {
 
    public String processOrder(String customerName, String customerEmail,
                              int customerAge, String customerAddress,
                              String productName, double productPrice,
                              int quantity, String couponCode,
                              boolean isExpress, String paymentMethod) {
 
        // Validation
        if (customerName == null || customerName.isEmpty()) {
            return "Error: Invalid customer name";
        }
 
        if (customerEmail == null || !customerEmail.contains("@")) {
            return "Error: Invalid email";
        }
 
        if (customerAge < 18) {
            return "Error: Customer must be 18+";
        }
 
        if (productName == null || productName.isEmpty()) {
            return "Error: Invalid product";
        }
 
        if (productPrice <= 0) {
            return "Error: Invalid price";
        }
 
        if (quantity <= 0) {
            return "Error: Invalid quantity";
        }
 
        // Calculate total
        double total = productPrice * quantity;
 
        // Apply coupon
        if (couponCode != null && !couponCode.isEmpty()) {
            if (couponCode.equals("SAVE10")) {
                total = total * 0.9;
            } else if (couponCode.equals("SAVE20")) {
                total = total * 0.8;
            } else if (couponCode.equals("FIRST")) {
                if (isFirstTimeCustomer(customerEmail)) {
                    total = total * 0.85;
                }
            }
        }
 
        // Express shipping
        double shipping = 0;
        if (isExpress) {
            if (total > 100) {
                shipping = 15.0;
            } else {
                shipping = 25.0;
            }
        } else {
            if (total > 50) {
                shipping = 5.0;
            } else {
                shipping = 10.0;
            }
        }
 
        total += shipping;
 
        // Payment processing
        if (paymentMethod.equals("CREDIT")) {
            // Credit card processing...
            // 20 lines of credit card logic
        } else if (paymentMethod.equals("DEBIT")) {
            // Debit card processing...
            // 15 lines of debit card logic
        } else if (paymentMethod.equals("PAYPAL")) {
            // PayPal processing...
            // 18 lines of PayPal logic
        }
 
        return "Order processed successfully. Total: $" + total;
    }
 
    private boolean isFirstTimeCustomer(String email) {
        // Database lookup logic...
        return false;
    }
}

Sua tarefa:

  1. Liste todos os code smells encontrados
  2. Classifique por severidade (Alto/Médio/Baixo)
  3. Propor plano de refatoração
  4. Estimar impacto da refatoração

🏋️ Exercício B: Refatoração Completa

// Refatore completamente este código aplicando todas as técnicas
public class ReportGenerator {
 
    public String generateSalesReport(Date startDate, Date endDate,
                                    String format, boolean includeCharts,
                                    String salesPersonFilter,
                                    String regionFilter) {
 
        StringBuilder report = new StringBuilder();
 
        // Header
        if (format.equals("HTML")) {
            report.append("<html><head><title>Sales Report</title></head><body>");
            report.append("<h1>Sales Report from " + startDate + " to " + endDate + "</h1>");
        } else if (format.equals("PDF")) {
            report.append("SALES REPORT\n");
            report.append("Period: " + startDate + " to " + endDate + "\n");
            report.append("=====================================\n");
        } else {
            report.append("Sales Report - " + startDate + " to " + endDate + "\n");
        }
 
        // Data fetching and processing
        List<Sale> sales = new ArrayList<>();
        Connection conn = DatabaseManager.getConnection();
 
        try {
            String sql = "SELECT * FROM sales WHERE sale_date BETWEEN ? AND ?";
 
            if (salesPersonFilter != null && !salesPersonFilter.isEmpty()) {
                sql += " AND salesperson = ?";
            }
 
            if (regionFilter != null && !regionFilter.isEmpty()) {
                sql += " AND region = ?";
            }
 
            PreparedStatement stmt = conn.prepareStatement(sql);
            stmt.setDate(1, startDate);
            stmt.setDate(2, endDate);
 
            int paramIndex = 3;
            if (salesPersonFilter != null && !salesPersonFilter.isEmpty()) {
                stmt.setString(paramIndex++, salesPersonFilter);
            }
            if (regionFilter != null && !regionFilter.isEmpty()) {
                stmt.setString(paramIndex, regionFilter);
            }
 
            ResultSet rs = stmt.executeQuery();
 
            while (rs.next()) {
                Sale sale = new Sale();
                sale.setId(rs.getInt("id"));
                sale.setAmount(rs.getDouble("amount"));
                sale.setSalesperson(rs.getString("salesperson"));
                sale.setRegion(rs.getString("region"));
                sale.setDate(rs.getDate("sale_date"));
                sales.add(sale);
            }
 
        } catch (SQLException e) {
            return "Error generating report: " + e.getMessage();
        }
 
        // Statistics calculation
        double totalAmount = 0;
        Map<String, Double> salesPersonTotals = new HashMap<>();
        Map<String, Double> regionTotals = new HashMap<>();
 
        for (Sale sale : sales) {
            totalAmount += sale.getAmount();
 
            String person = sale.getSalesperson();
            salesPersonTotals.put(person,
                salesPersonTotals.getOrDefault(person, 0.0) + sale.getAmount());
 
            String region = sale.getRegion();
            regionTotals.put(region,
                regionTotals.getOrDefault(region, 0.0) + sale.getAmount());
        }
 
        // Format output
        if (format.equals("HTML")) {
            report.append("<h2>Summary</h2>");
            report.append("<p>Total Sales: $" + String.format("%.2f", totalAmount) + "</p>");
            report.append("<p>Number of Transactions: " + sales.size() + "</p>");
 
            report.append("<h2>By Salesperson</h2><ul>");
            for (Map.Entry<String, Double> entry : salesPersonTotals.entrySet()) {
                report.append("<li>" + entry.getKey() + ": $" +
                            String.format("%.2f", entry.getValue()) + "</li>");
            }
            report.append("</ul>");
 
            report.append("<h2>By Region</h2><ul>");
            for (Map.Entry<String, Double> entry : regionTotals.entrySet()) {
                report.append("<li>" + entry.getKey() + ": $" +
                            String.format("%.2f", entry.getValue()) + "</li>");
            }
            report.append("</ul>");
 
            if (includeCharts) {
                report.append("<h2>Charts</h2>");
                report.append("<div>Chart data would be generated here...</div>");
            }
 
            report.append("</body></html>");
 
        } else if (format.equals("PDF")) {
            report.append("\nSUMMARY\n");
            report.append("Total Sales: $" + String.format("%.2f", totalAmount) + "\n");
            report.append("Number of Transactions: " + sales.size() + "\n\n");
 
            report.append("BY SALESPERSON\n");
            for (Map.Entry<String, Double> entry : salesPersonTotals.entrySet()) {
                report.append(entry.getKey() + ": $" +
                            String.format("%.2f", entry.getValue()) + "\n");
            }
 
            report.append("\nBY REGION\n");
            for (Map.Entry<String, Double> entry : regionTotals.entrySet()) {
                report.append(entry.getKey() + ": $" +
                            String.format("%.2f", entry.getValue()) + "\n");
            }
 
        } else {
            report.append("\nSummary:\n");
            report.append("Total Sales: $" + String.format("%.2f", totalAmount) + "\n");
            report.append("Transactions: " + sales.size() + "\n");
        }
 
        return report.toString();
    }
}

Meta da Refatoração:

  • Reduzir método principal para < 15 linhas
  • Eliminar duplicação de formatação
  • Separar responsabilidades (data access, processing, formatting)
  • Introduzir Strategy pattern para formatters
  • Melhorar tratamento de erro
  • Facilitar adição de novos formatos

Estrutura Final Esperada:

public class ReportGenerator {
 
    public String generateSalesReport(ReportRequest request) {
        // Método principal limpo
    }
 
    private List<Sale> fetchSalesData(ReportRequest request) { }
    private SalesStatistics calculateStatistics(List<Sale> sales) { }
    private String formatReport(SalesStatistics stats, ReportFormat format) { }
}

📖 Use este material como referência durante e após a aula!