📖 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
🔗 Links de Aprofundamento
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
- Duplicação = Bug futuro garantido quando regras mudarem
- Componentes reutilizáveis devem ser projetados para testar e manter
- Fail-fast com mensagens úteis economiza horas de debug
- 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