“Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.” — The Pragmatic Programmer, Andrew Hunt e David Thomas.
DRY não é sobre código duplicado — é sobre conhecimento duplicado. Dois trechos de código que fazem coisas diferentes podem ter a mesma estrutura. Dois trechos que fazem a mesma coisa por razões diferentes podem evoluir em direções opostas.
DRY vs WET — A Regra dos Três
WET significa “Write Everything Twice” (ou “We Enjoy Typing”). Escrever algo uma segunda vez é aceitável. Escrever pela terceira vez é o sinal para abstrair.
// PRIMEIRA VEZ — escreva normalmente
public class OrderService {
public BigDecimal calculateDiscount(Order order) {
if (order.customer().isVip()) {
return order.subtotal().multiply(BigDecimal.valueOf(0.10));
}
return BigDecimal.ZERO;
}
}
// SEGUNDA VEZ — pode ser coincidência, YAGNI, aguarde
public class QuoteService {
public BigDecimal estimateDiscount(Cart cart) {
if (cart.customer().isVip()) {
return cart.subtotal().multiply(BigDecimal.valueOf(0.10)); // mesma lógica
}
return BigDecimal.ZERO;
}
}
// TERCEIRA VEZ — hora de extrair
public class InvoiceService {
public BigDecimal invoiceDiscount(Invoice invoice) {
if (invoice.customer().isVip()) {
return invoice.subtotal().multiply(BigDecimal.valueOf(0.10)); // terceira vez!
}
return BigDecimal.ZERO;
}
}
// ✅ Rule of Three: na terceira, extraia a abstração
public class VipDiscountPolicy {
private static final BigDecimal VIP_RATE = BigDecimal.valueOf(0.10);
public BigDecimal calculate(Customer customer, BigDecimal subtotal) {
if (customer.isVip()) {
return subtotal.multiply(VIP_RATE);
}
return BigDecimal.ZERO;
}
}
// Agora OrderService, QuoteService e InvoiceService usam VipDiscountPolicy
// Se a taxa mudar de 10% para 15%, altera em um lugar sóTipos de Duplicação
Duplicação não é só código — existem vários tipos, cada um com tratamento diferente.
Duplicação de lógica: a mesma regra de negócio implementada em vários lugares.
// ❌ Regra "pedido é elegível para frete grátis acima de R$200"
// implementada em 3 lugares
public class CartService {
public boolean hasFreShipping(Cart cart) {
return cart.total().compareTo(BigDecimal.valueOf(200)) >= 0; // lugar 1
}
}
public class CheckoutController {
public boolean showFreeShippingBanner(BigDecimal total) {
return total.compareTo(BigDecimal.valueOf(200)) >= 0; // lugar 2
}
}
public class OrderEntity {
public boolean isEligibleForFreeShipping() {
return getTotal().compareTo(BigDecimal.valueOf(200)) >= 0; // lugar 3
}
}
// ✅ Extraia a regra com nome de negócio
public class FreeShippingPolicy {
private static final BigDecimal THRESHOLD = BigDecimal.valueOf(200);
public boolean isEligible(BigDecimal orderTotal) {
return orderTotal.compareTo(THRESHOLD) >= 0;
}
}Duplicação de dados: a mesma informação em múltiplas estruturas sem fonte única de verdade.
// ❌ Preço do produto duplicado em Product e CartItem
public class Product {
private String id;
private String name;
private BigDecimal price; // fonte de verdade
}
public class CartItem {
private String productId;
private String productName; // duplicado de Product.name
private BigDecimal price; // duplicado de Product.price — qual é o certo?
private int quantity;
}
// ✅ CartItem referencia o produto, não duplica seus dados
// (snapshot de preço é legítimo se o preço pode mudar — mas documente essa intenção)
public class CartItem {
private final String productId;
private final BigDecimal priceAtTimeOfAdd; // nome explicita que é snapshot intencional
private int quantity;
public static CartItem from(Product product, int quantity) {
return new CartItem(product.id(), product.currentPrice(), quantity);
}
}Duplicação de representação: mesma estrutura de dados em camadas diferentes sem motivo.
// ❌ ProductDTO = Product com os mesmos campos — por quê existem dois?
public class Product {
private String id;
private String name;
private BigDecimal price;
private String category;
}
public class ProductDTO {
private String id;
private String name;
private BigDecimal price;
private String category;
// 100% idêntico — nenhuma razão para existir separado
}
// ✅ Use o domínio diretamente na API se não há transformação necessária
// OU garanta que o DTO serve a um propósito distinto (versioning, projeção parcial)
public record ProductSummaryResponse(String id, String name, BigDecimal price) {
// Projeção: apenas 3 campos dos 10 de Product — justificativa clara
public static ProductSummaryResponse from(Product p) {
return new ProductSummaryResponse(p.id(), p.name(), p.price());
}
}Quando NÃO Seguir DRY — Coupling Acidental
Este é o erro mais comum: abstrair código que parece igual mas representa conceitos diferentes.
// Dois métodos com a mesma implementação hoje
public class UserRegistrationService {
public boolean isValidEmail(String email) {
return email != null && email.contains("@") && email.contains(".");
}
}
public class MarketingEmailService {
public boolean isValidEmail(String email) {
return email != null && email.contains("@") && email.contains(".");
}
}
// ❌ ERRADO — abstrair "porque são iguais"
public class EmailValidator {
public static boolean isValid(String email) {
return email != null && email.contains("@") && email.contains(".");
}
}
// Agora os dois serviços compartilham a mesma validação
// Mas eles têm preocupações DIFERENTES:
// UserRegistrationService: valida para criar conta segura
// amanhã pode precisar verificar domínios banidos, LGPD, etc.
// MarketingEmailService: valida para entregabilidade de campanha
// amanhã pode verificar DNS MX record, bounce rate do domínio, etc.
// Ao compartilhar, qualquer mudança em uma afeta a outra — coupling acidental
// ✅ CORRETO — deixar duplicado com nomes que explicam a intenção diferente
public class UserRegistrationService {
private boolean isEmailValidForRegistration(String email) {
return email != null && email.contains("@") && email.contains(".");
// evolui com regras de segurança e compliance
}
}
public class MarketingEmailService {
private boolean isEmailDeliverable(String email) {
return email != null && email.contains("@") && email.contains(".");
// evolui com regras de entregabilidade
}
}Regra: antes de abstrair, responda: “se um dos usos mudar, o outro também deve mudar?”. Se não, deixe duplicado.
Abstrações Prematuras — Exemplos de Over-Engineering
// ❌ Over-engineering — criando "flexibilidade" sem necessidade
// Classe genérica que tenta resolver todos os casos futuros imagináveis
public class GenericEntityProcessor<T, R, V extends Validator<T>, M extends Mapper<T, R>> {
private final V validator;
private final M mapper;
private final Repository<T> repository;
private final EventPublisher eventPublisher;
public R process(T input, ProcessingContext ctx, ProcessingOptions options) {
if (options.shouldValidate()) validator.validate(input);
if (options.shouldPersist()) repository.save(input);
if (options.shouldPublishEvent()) eventPublisher.publish(buildEvent(input, ctx));
return mapper.map(input);
}
}
// ✅ Simples e direto — fácil de entender e modificar
public class CreateProductUseCase {
public ProductId execute(CreateProductCommand cmd) {
Product product = new Product(cmd.name(), cmd.price(), cmd.category());
productRepository.save(product);
eventPublisher.publish(new ProductCreated(product.id(), product.name()));
return product.id();
}
}
// Extrai abstração QUANDO surgir o segundo use case com padrão similar
// Não antesDRY com Herança vs Composição
Herança é uma forma poderosa de compartilhar código, mas gera acoplamento forte. Prefira composição quando o objetivo é apenas reutilização.
// ❌ Herança para reutilizar — cria acoplamento desnecessário
public abstract class BaseService {
protected final Logger log = LoggerFactory.getLogger(getClass());
protected final ApplicationEventPublisher events;
protected void publishEvent(Object event) {
log.info("Publicando evento: {}", event.getClass().getSimpleName());
events.publishEvent(event);
}
protected <T> T findOrThrow(Optional<T> optional, String message) {
return optional.orElseThrow(() -> new NotFoundException(message));
}
}
// Todos os services herdam de BaseService — herança por conveniência, não por relação "é um"
public class OrderService extends BaseService { ... }
public class CustomerService extends BaseService { ... }
public class ProductService extends BaseService { ... }
// ✅ Composição — injete as dependências que precisar
public class OrderService {
private final Logger log = LoggerFactory.getLogger(OrderService.class);
private final ApplicationEventPublisher events;
private final EntityFinder finder; // helper injetado onde necessário
// Só usa o que precisa, sem carregar herança pesada
}
// Helper simples para busca com throw
@Component
public class EntityFinder {
public <T> T findOrThrow(Optional<T> optional, String entity, Object id) {
return optional.orElseThrow(() ->
new EntityNotFoundException(entity + " não encontrado: " + id)
);
}
}DRY em SQL — Views e Funções
-- ❌ Mesma lógica de cálculo repetida em múltiplas queries
SELECT
o.id,
SUM(oi.quantity * oi.unit_price) AS subtotal,
CASE WHEN c.type = 'VIP' THEN SUM(oi.quantity * oi.unit_price) * 0.90
ELSE SUM(oi.quantity * oi.unit_price)
END AS total_with_discount
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN customers c ON c.id = o.customer_id
GROUP BY o.id, c.type;
-- (essa mesma query aparece em relatório mensal, dashboard e faturamento)
-- ✅ View como única representação
CREATE OR REPLACE VIEW order_totals AS
SELECT
o.id AS order_id,
o.customer_id,
c.type AS customer_type,
SUM(oi.quantity * oi.unit_price) AS subtotal,
CASE WHEN c.type = 'VIP'
THEN SUM(oi.quantity * oi.unit_price) * 0.90
ELSE SUM(oi.quantity * oi.unit_price)
END AS total
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN customers c ON c.id = o.customer_id
GROUP BY o.id, o.customer_id, c.type;
-- Agora todas as queries usam a view
SELECT * FROM order_totals WHERE order_id = $1;
SELECT customer_id, SUM(total) FROM order_totals GROUP BY customer_id;DRY em Configuração e Infraestrutura
# ❌ Repetição de configuração em docker-compose
services:
api:
environment:
- SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/ecommerce
- SPRING_DATASOURCE_USERNAME=dev
- SPRING_DATASOURCE_PASSWORD=dev
- SPRING_KAFKA_BOOTSTRAP_SERVERS=kafka:9092
worker:
environment:
- SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/ecommerce # repetido
- SPRING_DATASOURCE_USERNAME=dev # repetido
- SPRING_DATASOURCE_PASSWORD=dev # repetido
- SPRING_KAFKA_BOOTSTRAP_SERVERS=kafka:9092 # repetido
# ✅ Usar arquivo .env ou extension fields
# .env
POSTGRES_URL=jdbc:postgresql://postgres:5432/ecommerce
POSTGRES_USER=dev
POSTGRES_PASS=dev
KAFKA_URL=kafka:9092
# docker-compose.yml
services:
api:
env_file: .env
worker:
env_file: .env # mesmo arquivo de configuraçãoDRY em TypeScript — Tipos e Validações
// ❌ Tipo duplicado entre domínio e API
// domain/types.ts
interface Product {
id: string;
name: string;
price: number;
stock: number;
category: string;
}
// api/types.ts
interface ProductResponse {
id: string; // duplicado
name: string; // duplicado
price: number; // duplicado
stock: number; // duplicado
category: string; // duplicado
}
// ✅ Derive tipos a partir da fonte de verdade
// domain/types.ts — fonte de verdade
interface Product {
id: string;
name: string;
price: number;
stock: number;
category: string;
internalNotes: string; // campo interno, não deve ir para API
}
// api/types.ts — derivado, sem duplicação
type ProductResponse = Omit<Product, 'internalNotes'>;
// View específica para listagem
type ProductSummary = Pick<Product, 'id' | 'name' | 'price'>;
// ✅ Validação com Zod — esquema é a fonte de verdade para tipo E validação
import { z } from 'zod';
// Define uma vez — gera tipo TypeScript E validação runtime
const CreateProductSchema = z.object({
name: z.string().min(1).max(200),
price: z.number().positive(),
category: z.string(),
});
// Tipo derivado do schema — nunca fica dessincronizado
type CreateProductRequest = z.infer<typeof CreateProductSchema>;
// Usa em qualquer lugar que precise validar
function validateCreateProduct(data: unknown): CreateProductRequest {
return CreateProductSchema.parse(data); // lança se inválido
}