📖 Material de Apoio - Reutilização de Código em Java

🎯 Guia Rápido: Princípio DRY

Definição

DRY (Don’t Repeat Yourself): Cada piece de conhecimento deve ter uma representação única, não-ambígua e autoritativa dentro de um sistema.

Regra de Ouro

“Toda informação deve ser expressa uma única vez no sistema. Mudanças requerem alterar apenas um local.”


💡 Conceitos Essenciais

1. Tipos de Duplicação

🔴 Duplicação Forçada (Forced Duplication)

// Problema: Documentação e código em lugares diferentes
/**
 * Email deve ter formato: usuario@dominio
 * Máximo 254 caracteres segundo RFC 5321
 */
public boolean validarEmail(String email) {
    return email.matches("^[A-Za-z0-9+_.-]+@(.+)$")
           && email.length() <= 254;
}

🟡 Duplicação Inadvertida (Inadvertent Duplication)

// Problema: Mesma validação, lógicas diferentes
public class LoginValidator {
    boolean isValidEmail(String email) {
        return email.contains("@") && email.length() > 5;
    }
}
 
public class SubscriptionValidator {
    boolean emailValid(String email) {
        return email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
    }
}

🟠 Duplicação Impatiente (Impatient Duplication)

// Problema: "É mais rápido copiar que reutilizar"
public void processarPedido(Pedido pedido) {
    // Validação copiada de outro método
    if (pedido.getEmail() == null || pedido.getEmail().trim().isEmpty()) {
        throw new IllegalArgumentException("Email obrigatório");
    }
    if (!pedido.getEmail().contains("@")) {
        throw new IllegalArgumentException("Email inválido");
    }
    // ... resto do processamento
}

🟢 Duplicação de Desenvolvedores (Interdeveloper Duplication)

// Problema: Equipes diferentes criam soluções iguais
// Time A:
public class EmailUtils {
    public static boolean isValidEmail(String email) { /* implementação A */ }
}
 
// Time B:
public class ValidationHelper {
    public static boolean validateEmail(String email) { /* implementação B */ }
}

2. O Custo Real da Duplicação

Manutenção Multiplicada

// ❌ ANTES: 5 locais diferentes para atualizar
public class UserValidator {
    boolean isEmailValid(String email) {
        return email.matches("^[A-Za-z0-9+_.-]+@(.+)$"); // Local 1
    }
}
 
public class ContactValidator {
    boolean validateEmail(String email) {
        return email.matches("^[A-Za-z0-9+_.-]+@(.+)$"); // Local 2
    }
}
 
public class NewsletterService {
    private boolean emailOk(String email) {
        return email.matches("^[A-Za-z0-9+_.-]+@(.+)$"); // Local 3
    }
}
// + 2 outros locais...
 
// ✅ DEPOIS: 1 local centralizado
public class EmailValidator {
    private static final Pattern EMAIL_PATTERN =
        Pattern.compile("^[A-Za-z0-9+_.-]+@(.+)$");
 
    public static boolean isValid(String email) {
        return EMAIL_PATTERN.matcher(email).matches();
    }
}

Inconsistência Comportamental

// 🐛 BUG REAL: Comportamentos diferentes no mesmo sistema
public class CadastroController {
    // Aceita emails com maiúsculas
    boolean validarEmail(String email) {
        return email.toLowerCase().matches("^[a-z0-9+_.-]+@(.+)$");
    }
}
 
public class LoginController {
    // Rejeita emails com maiúsculas
    boolean validarEmail(String email) {
        return email.matches("^[a-z0-9+_.-]+@(.+)$");
    }
}
// Resultado: usuários conseguem cadastrar mas não conseguem fazer login!

⚙️ Estratégias de Eliminação

1. Extração de Métodos

Antes da Extração

public class CadastroService {
    public void cadastrarUsuario(Usuario user) {
        // Validação inline repetida
        if (user.getNome() == null || user.getNome().trim().isEmpty()) {
            throw new IllegalArgumentException("Nome obrigatório");
        }
        if (user.getNome().length() < 2) {
            throw new IllegalArgumentException("Nome muito curto");
        }
        if (user.getNome().length() > 100) {
            throw new IllegalArgumentException("Nome muito longo");
        }
        // ... lógica do cadastro
    }
 
    public void editarUsuario(Usuario user) {
        // Mesma validação duplicada
        if (user.getNome() == null || user.getNome().trim().isEmpty()) {
            throw new IllegalArgumentException("Nome obrigatório");
        }
        if (user.getNome().length() < 2) {
            throw new IllegalArgumentException("Nome muito curto");
        }
        if (user.getNome().length() > 100) {
            throw new IllegalArgumentException("Nome muito longo");
        }
        // ... lógica da edição
    }
}

Depois da Extração

public class CadastroService {
    public void cadastrarUsuario(Usuario user) {
        validarNome(user.getNome());
        // ... lógica do cadastro
    }
 
    public void editarUsuario(Usuario user) {
        validarNome(user.getNome());
        // ... lógica da edição
    }
 
    // Centralização da regra
    private void validarNome(String nome) {
        if (nome == null || nome.trim().isEmpty()) {
            throw new IllegalArgumentException("Nome obrigatório");
        }
        if (nome.length() < 2) {
            throw new IllegalArgumentException("Nome muito curto");
        }
        if (nome.length() > 100) {
            throw new IllegalArgumentException("Nome muito longo");
        }
    }
}

2. Criação de Classes Utilitárias

Validador Centralizado Completo

public class Validators {
 
    // CPF com normalização automática
    public static class CpfValidator {
        private static final Pattern CPF_PATTERN =
            Pattern.compile("\\d{3}\\.\\d{3}\\.\\d{3}-\\d{2}");
 
        public static boolean isValid(String cpf) {
            return !StringUtils.isBlank(cpf)
                   && isValidFormat(normalize(cpf))
                   && isValidCheckDigits(normalize(cpf));
        }
 
        public static String normalize(String cpf) {
            return StringUtils.isBlank(cpf) ? ""
                   : cpf.replaceAll("\\D", "");
        }
 
        static boolean isValidFormat(String cpf) {
            return cpf.length() == 11 && !cpf.matches("(\\d)\\1{10}");
        }
 
        static boolean isValidCheckDigits(String cpf) {
            return calculateCheckDigit(cpf, 10) == getDigit(cpf, 9)
                   && calculateCheckDigit(cpf, 11) == getDigit(cpf, 10);
        }
 
        private static int calculateCheckDigit(String cpf, int factor) {
            int sum = 0;
            for (int i = 0; i < factor - 1; i++) {
                sum += getDigit(cpf, i) * (factor - i);
            }
            return ((sum * 10) % 11) % 10;
        }
 
        private static int getDigit(String cpf, int index) {
            return Character.getNumericValue(cpf.charAt(index));
        }
    }
 
    // Email com suporte internacional
    public static class EmailValidator {
        private static final Pattern EMAIL_PATTERN = Pattern.compile(
            "^[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+)*" +
            "@(?:[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?\\.)+[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$"
        );
 
        public static boolean isValid(String email) {
            return !StringUtils.isBlank(email)
                   && email.length() <= 254 // RFC 5321 limit
                   && EMAIL_PATTERN.matcher(email.trim().toLowerCase()).matches();
        }
 
        public static String normalize(String email) {
            return StringUtils.isBlank(email) ? ""
                   : email.trim().toLowerCase();
        }
    }
}

3. Configuração Externa para Regras

Validações Parametrizáveis

// ValidationConfig.properties
user.name.min.length=2
user.name.max.length=100
user.email.max.length=254
user.cpf.required=true
 
// Validador configurável
public class ConfigurableValidator {
    private final Properties config;
 
    public ConfigurableValidator(Properties config) {
        this.config = config;
    }
 
    public void validateUserName(String name) {
        if (StringUtils.isBlank(name)) {
            throw new IllegalArgumentException("Nome obrigatório");
        }
 
        int minLength = Integer.parseInt(config.getProperty("user.name.min.length", "1"));
        int maxLength = Integer.parseInt(config.getProperty("user.name.max.length", "255"));
 
        if (name.length() < minLength) {
            throw new IllegalArgumentException(
                String.format("Nome deve ter pelo menos %d caracteres", minLength)
            );
        }
 
        if (name.length() > maxLength) {
            throw new IllegalArgumentException(
                String.format("Nome deve ter no máximo %d caracteres", maxLength)
            );
        }
    }
}

🧪 Padrões de Teste para Código Reutilizável

Testes Parametrizados para Validadores

@ParameterizedTest
@ValueSource(strings = {
    "11144477735",     // Formato básico válido
    "111.444.777-35",   // Formato com máscara válido
    "000.000.001-91"    // Caso extremo válido
})
void shouldAcceptValidCpf(String cpf) {
    assertTrue(CpfValidator.isValid(cpf),
               () -> "CPF válido rejeitado: " + cpf);
}
 
@ParameterizedTest
@ValueSource(strings = {
    "11111111111",      // Sequência inválida
    "123.456.789-00",   // Check digits inválidos
    "123",              // Muito curto
    "",                 // Vazio
    "   "               // Só espaços
})
void shouldRejectInvalidCpf(String cpf) {
    assertFalse(CpfValidator.isValid(cpf),
                () -> "CPF inválido aceito: " + cpf);
}
 
// Teste de normalização
@Test
void shouldNormalizeCpfFormats() {
    assertAll(
        () -> assertEquals("11144477735",
                          CpfValidator.normalize("111.444.777-35")),
        () -> assertEquals("11144477735",
                          CpfValidator.normalize("111 444 777 35")),
        () -> assertEquals("",
                          CpfValidator.normalize(null)),
        () -> assertEquals("",
                          CpfValidator.normalize("   "))
    );
}

🚀 Melhores Práticas

1. Design para Reutilização

✅ Faça:

// Interface clara e bem documentada
public class EmailValidator {
    /**
     * Valida formato de email seguindo RFC 5322.
     * @param email string a ser validada (pode ser null/empty)
     * @return true se email é válido, false caso contrário
     */
    public static boolean isValid(String email) {
        return !StringUtils.isBlank(email) && /* validação */;
    }
 
    /**
     * Normaliza email para comparação.
     * @param email string a ser normalizada
     * @return email em lowercase e sem espaços, ou string vazia se input inválido
     */
    public static String normalize(String email) {
        return StringUtils.isBlank(email) ? "" : email.trim().toLowerCase();
    }
}

❌ Evite:

// Métodos com efeitos colaterais escondidos
public class EmailValidator {
    private static List<String> invalidEmails = new ArrayList<>(); // Estado oculto!
 
    public static boolean isValid(String email) {
        boolean valid = /* validação */;
        if (!valid) {
            invalidEmails.add(email); // Side effect perigoso!
        }
        return valid;
    }
}

2. Fail Fast com Mensagens Úteis

✅ Faça:

public static void validateEmail(String email) {
    if (email == null) {
        throw new IllegalArgumentException("Email não pode ser null");
    }
    if (email.trim().isEmpty()) {
        throw new IllegalArgumentException("Email não pode estar vazio");
    }
    if (email.length() > 254) {
        throw new IllegalArgumentException(
            String.format("Email muito longo: %d caracteres (máximo: 254)",
                         email.length())
        );
    }
    if (!EMAIL_PATTERN.matcher(email).matches()) {
        throw new IllegalArgumentException(
            String.format("Formato de email inválido: '%s'", email)
        );
    }
}

3. Testabilidade desde o Design

✅ Componentes Testáveis:

// Sem dependências externas
public class CpfValidator {
    // Métodos package-private para testar internals
    static boolean isValidFormat(String cpf) { /* ... */ }
    static boolean isValidCheckDigits(String cpf) { /* ... */ }
 
    // Método público que combina validações
    public static boolean isValid(String cpf) {
        String normalized = normalize(cpf);
        return isValidFormat(normalized) && isValidCheckDigits(normalized);
    }
}
 
// Testes podem validar cada parte independentemente
@Test
void shouldValidateFormatIndependently() {
    assertFalse(CpfValidator.isValidFormat("123"));        // Muito curto
    assertTrue(CpfValidator.isValidFormat("12345678901"));  // Formato OK
    assertFalse(CpfValidator.isValidFormat("11111111111")); // Sequência
}

📊 Métricas de Qualidade DRY

Indicadores de Sucesso

  • Localização única: Alteração em regra de negócio requer mudança em apenas 1 arquivo
  • Comportamento consistente: Mesma entrada produz mesma saída em todo sistema
  • Facilidade de teste: Validadores podem ser testados independentemente
  • Baixo acoplamento: Componentes não dependem de contexto específico

Sinais de Alerta

  • Busca por “copiar validação” em histórico de commits
  • Bugs de inconsistência entre funcionalidades similares
  • Pull requests grandes modificando “mesma lógica em vários arquivos”
  • Discussões de equipe sobre “qual validação usar”

🎯 Exercícios de Fixação

Exercício 1: Identificação de Duplicação

Analise o código abaixo e identifique todas as duplicações:

public class UserController {
    public ResponseEntity<?> createUser(@RequestBody CreateUserRequest request) {
        if (request.getEmail() == null || request.getEmail().trim().isEmpty()) {
            return ResponseEntity.badRequest().body("Email obrigatório");
        }
        if (request.getEmail().length() > 200) {
            return ResponseEntity.badRequest().body("Email muito longo");
        }
        if (!request.getEmail().contains("@")) {
            return ResponseEntity.badRequest().body("Email inválido");
        }
 
        // ... criar usuário
        return ResponseEntity.ok().build();
    }
 
    public ResponseEntity<?> updateUser(@RequestBody UpdateUserRequest request) {
        if (request.getEmail() == null || request.getEmail().trim().isEmpty()) {
            return ResponseEntity.badRequest().body("Email obrigatório");
        }
        if (request.getEmail().length() > 200) {
            return ResponseEntity.badRequest().body("Email muito longo");
        }
        if (!request.getEmail().contains("@")) {
            return ResponseEntity.badRequest().body("Email inválido");
        }
 
        // ... atualizar usuário
        return ResponseEntity.ok().build();
    }
}

Exercício 2: Refatoração DRY

Refatore o código do Exercício 1 aplicando princípios DRY.

Exercício 3: Implementação de Validador

Implemente um PhoneValidator seguindo os padrões da aula:

  • Suporte a múltiplos formatos: (11) 99999-9999, 11999999999, +5511999999999
  • Método normalize() para padronizar formato
  • Validação fail-fast com mensagens específicas
  • Testes parametrizados completos

Documentação Oficial

Artigos Essenciais

Código de Referência


Resumo Executivo

Mensagem-chave da Aula

“DRY não é sobre evitar digitar código duas vezes. É sobre tomar uma decisão uma vez e garantir que ela seja aplicada consistentemente em todo o sistema.”

Principais Takeaways

  1. Duplicação = Bug futuro garantido quando regras mudarem
  2. Componentes reutilizáveis devem ser projetados para testar e manter
  3. Fail-fast com mensagens úteis economiza horas de debug
  4. Centralização permite evolução controlada do sistema

Próximos Passos (Aula 03)

  • Refatoração segura de sistemas existentes
  • Técnicas de extração de métodos e classes
  • Como manter compatibilidade durante mudanças estruturais