Skill de Padrões de Projeto
Referência prática de padrões de projeto para Java com exemplos modernos.
Quando Usar
- O usuário pede para implementar um padrão específico
- Projetando componentes extensíveis/flexíveis
- Refatorando estruturas de código rígidas
- Revisão de código sugere uso de padrões
Referência Rápida: Quando Usar o Quê
| Problema | Padrão |
|---|---|
| Construção de objeto complexo | Builder |
| Criar objetos sem especificar a classe | Factory |
| Múltiplos algoritmos, trocar em tempo de execução | Strategy |
| Adicionar comportamento sem alterar a classe | Decorator |
| Notificar múltiplos objetos de mudanças | Observer |
| Garantir instância única | Singleton |
| Converter interfaces incompatíveis | Adapter |
| Definir esqueleto de algoritmo | Template Method |
Padrões Criacionais
Builder
Use quando: O objeto tem muitos parâmetros, alguns opcionais.
// ❌ Antipadrão de construtor telescópico
public class User {
public User(String name) { }
public User(String name, String email) { }
public User(String name, String email, int age) { }
public User(String name, String email, int age, String phone) { }
// ... explosão de construtores
}
// ✅ Padrão Builder
public class User {
private final String name; // obrigatório
private final String email; // obrigatório
private final int age; // opcional
private final String phone; // opcional
private final String address; // opcional
private User(Builder builder) {
this.name = builder.name;
this.email = builder.email;
this.age = builder.age;
this.phone = builder.phone;
this.address = builder.address;
}
public static Builder builder(String name, String email) {
return new Builder(name, email);
}
public static class Builder {
// Obrigatórios
private final String name;
private final String email;
// Opcionais com valores padrão
private int age = 0;
private String phone = "";
private String address = "";
private Builder(String name, String email) {
this.name = name;
this.email = email;
}
public Builder age(int age) {
this.age = age;
return this;
}
public Builder phone(String phone) {
this.phone = phone;
return this;
}
public Builder address(String address) {
this.address = address;
return this;
}
public User build() {
return new User(this);
}
}
}
// Uso
User user = User.builder("João", "[email protected]")
.age(30)
.phone("+551234567890")
.build();
Com Lombok:
@Builder
@Getter
public class User {
private final String name;
private final String email;
@Builder.Default private int age = 0;
private String phone;
}
Factory Method
Use quando: Precisar criar objetos sem especificar a classe exata.
// ✅ Padrão Factory Method
public interface Notification {
void send(String message);
}
public class EmailNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Email: " + message);
}
}
public class SmsNotification implements Notification {
@Override
public void send(String message) {
System.out.println("SMS: " + message);
}
}
public class PushNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Push: " + message);
}
}
// Factory
public class NotificationFactory {
public static Notification create(String type) {
return switch (type.toUpperCase()) {
case "EMAIL" -> new EmailNotification();
case "SMS" -> new SmsNotification();
case "PUSH" -> new PushNotification();
default -> throw new IllegalArgumentException("Tipo desconhecido: " + type);
};
}
}
// Uso
Notification notification = NotificationFactory.create("EMAIL");
notification.send("Olá!");
Com Spring (preferido):
public interface NotificationSender {
void send(String message);
String getType();
}
@Component
public class EmailSender implements NotificationSender {
@Override public void send(String message) { /* ... */ }
@Override public String getType() { return "EMAIL"; }
}
@Component
public class SmsSender implements NotificationSender {
@Override public void send(String message) { /* ... */ }
@Override public String getType() { return "SMS"; }
}
@Component
public class NotificationFactory {
private final Map<String, NotificationSender> senders;
public NotificationFactory(List<NotificationSender> senderList) {
this.senders = senderList.stream()
.collect(Collectors.toMap(
NotificationSender::getType,
Function.identity()
));
}
public NotificationSender getSender(String type) {
return Optional.ofNullable(senders.get(type))
.orElseThrow(() -> new IllegalArgumentException("Desconhecido: " + type));
}
}
Singleton
Use quando: Exatamente uma instância é necessária (use com moderação!).
// ✅ Singleton moderno (baseado em enum, thread-safe)
public enum DatabaseConnection {
INSTANCE;
private Connection connection;
DatabaseConnection() {
// Inicializar conexão
}
public Connection getConnection() {
return connection;
}
}
// Uso
Connection conn = DatabaseConnection.INSTANCE.getConnection();
Com Spring (preferido):
@Component // Escopo padrão é singleton
public class DatabaseConnection {
// Spring gerencia a instância única
}
Aviso: Singletons podem ser problemáticos:
- Difícil de testar (estado global)
- Dependências ocultas
- Considere injeção de dependência como alternativa
Padrões Comportamentais
Strategy
Use quando: Múltiplos algoritmos para a mesma operação, necessidade de trocar em tempo de execução.
// ✅ Padrão Strategy
public interface PaymentStrategy {
void pay(BigDecimal amount);
}
public class CreditCardPayment implements PaymentStrategy {
private final String cardNumber;
public CreditCardPayment(String cardNumber) {
this.cardNumber = cardNumber;
}
@Override
public void pay(BigDecimal amount) {
System.out.println("Pago " + amount + " com cartão " + cardNumber);
}
}
public class PayPalPayment implements PaymentStrategy {
private final String email;
public PayPalPayment(String email) {
this.email = email;
}
@Override
public void pay(BigDecimal amount) {
System.out.println("Pago " + amount + " via PayPal: " + email);
}
}
public class CryptoPayment implements PaymentStrategy {
private final String walletAddress;
public CryptoPayment(String walletAddress) {
this.walletAddress = walletAddress;
}
@Override
public void pay(BigDecimal amount) {
System.out.println("Pago " + amount + " para carteira: " + walletAddress);
}
}
// Contexto
public class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout(BigDecimal total) {
paymentStrategy.pay(total);
}
}
// Uso
ShoppingCart cart = new ShoppingCart();
cart.setPaymentStrategy(new CreditCardPayment("4111-1111-1111-1111"));
cart.checkout(new BigDecimal("99.99"));
// Trocar estratégia em tempo de execução
cart.setPaymentStrategy(new PayPalPayment("[email protected]"));
cart.checkout(new BigDecimal("49.99"));
Com Java 8+ (funcional):
// Strategy como interface funcional
@FunctionalInterface
public interface PaymentStrategy {
void pay(BigDecimal amount);
}
// Uso com lambdas
PaymentStrategy creditCard = amount ->
System.out.println("Pagamento com cartão: " + amount);
PaymentStrategy paypal = amount ->
System.out.println("Pagamento com PayPal: " + amount);
cart.setPaymentStrategy(creditCard);
Observer
Use quando: Objetos precisam ser notificados de mudanças em outro objeto.
// ✅ Padrão Observer (Java moderno)
public interface OrderObserver {
void onOrderPlaced(Order order);
}
public class OrderService {
private final List<OrderObserver> observers = new ArrayList<>();
public void addObserver(OrderObserver observer) {
observers.add(observer);
}
public void removeObserver(OrderObserver observer) {
observers.remove(observer);
}
public void placeOrder(Order order) {
// Processar pedido
saveOrder(order);
// Notificar todos os observadores
observers.forEach(observer -> observer.onOrderPlaced(order));
}
}
// Observadores
public class InventoryService implements OrderObserver {
@Override
public void onOrderPlaced(Order order) {
// Reduzir estoque
order.getItems().forEach(item ->
reduceStock(item.getProductId(), item.getQuantity())
);
}
}
public class EmailNotificationService implements OrderObserver {
@Override
public void onOrderPlaced(Order order) {
sendConfirmationEmail(order.getCustomerEmail(), order);
}
}
public class AnalyticsService implements OrderObserver {
@Override
public void onOrderPlaced(Order order) {
trackOrderEvent(order);
}
}
// Configuração
OrderService orderService = new OrderService();
orderService.addObserver(new InventoryService());
orderService.addObserver(new EmailNotificationService());
orderService.addObserver(new AnalyticsService());
Com Eventos Spring (preferido):
// Evento
public record OrderPlacedEvent(Order order) {}
// Editor
@Service
public class OrderService {
private final ApplicationEventPublisher eventPublisher;
public void placeOrder(Order order) {
saveOrder(order);
eventPublisher.publishEvent(new OrderPlacedEvent(order));
}
}
// Ouvintes (observadores)
@Component
public class InventoryListener {
@EventListener
public void handleOrderPlaced(OrderPlacedEvent event) {
// Reduzir estoque
}
}
@Component
public class EmailListener {
@EventListener
public void handleOrderPlaced(OrderPlacedEvent event) {
// Enviar email
}
@EventListener
@Async // Processamento assíncrono
public void handleOrderPlacedAsync(OrderPlacedEvent event) {
// Enviar email de forma assíncrona
}
}
Template Method
Use quando: Definir esqueleto de algoritmo, deixar que subclasses preencham as etapas.
// ✅ Padrão Template Method
public abstract class DataProcessor {
// Método template - define o algoritmo
public final void process() {
readData();
processData();
writeData();
if (shouldNotify()) {
notifyCompletion();
}
}
// Etapas a serem implementadas pelas subclasses
protected abstract void readData();
protected abstract void processData();
protected abstract void writeData();
// Gancho - substituição opcional
protected boolean shouldNotify() {
return true;
}
protected void notifyCompletion() {
System.out.println("Processamento concluído!");
}
}
public class CsvDataProcessor extends DataProcessor {
@Override
protected void readData() {
System.out.println("Lendo arquivo CSV...");
}
@Override
protected void processData() {
System.out.println("Processando dados CSV...");
}
@Override
protected void writeData() {
System.out.println("Escrevendo no banco de dados...");
}
}
public class ApiDataProcessor extends DataProcessor {
@Override
protected void readData() {
System.out.println("Buscando da API...");
}
@Override
protected void processData() {
System.out.println("Transformando resposta da API...");
}
@Override
protected void writeData() {
System.out.println("Escrevendo em cache...");
}
@Override
protected boolean shouldNotify() {
return false; // Substitui o gancho
}
}
// Uso
DataProcessor csvProcessor = new CsvDataProcessor();
csvProcessor.process();
DataProcessor apiProcessor = new ApiDataProcessor();
apiProcessor.process();
Padrões Estruturais
Decorator
Use quando: Adicionar comportamento dinamicamente sem modificar classes existentes.
// ✅ Padrão Decorator
public interface Coffee {
String getDescription();
BigDecimal getCost();
}
public class SimpleCoffee implements Coffee {
@Override
public String getDescription() {
return "Café";
}
@Override
public BigDecimal getCost() {
return new BigDecimal("2.00");
}
}
// Decorador base
public abstract class CoffeeDecorator implements Coffee {
protected final Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public String getDescription() {
return coffee.getDescription();
}
@Override
public BigDecimal getCost() {
return coffee.getCost();
}
}
// Decoradores concretos
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Leite";
}
@Override
public BigDecimal getCost() {
return coffee.getCost().add(new BigDecimal("0.50"));
}
}
public class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Açúcar";
}
@Override
public BigDecimal getCost() {
return coffee.getCost().add(new BigDecimal("0.20"));
}
}
public class WhippedCreamDecorator extends CoffeeDecorator {
public WhippedCreamDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Chantily";
}
@Override
public BigDecimal getCost() {
return coffee.getCost().add(new BigDecimal("0.70"));
}
}
// Uso
Coffee coffee = new SimpleCoffee();
System.out.println(coffee.getDescription() + " -> " + coffee.getCost());
coffee = new MilkDecorator(coffee);
System.out.println(coffee.getDescription() + " -> " + coffee.getCost());
coffee = new SugarDecorator(coffee);
System.out.println(coffee.getDescription() + " -> " + coffee.getCost());
coffee = new WhippedCreamDecorator(coffee);
System.out.println(coffee.getDescription() + " -> " + coffee.getCost());
Adapter
Use quando: Tornar classes incompatíveis trabalharem juntas.
// ✅ Padrão Adapter
// Serviço externo (incompatível)
public class ExternalPaymentService {
public void processPayment(String currency, double amount) {
System.out.println("Pagamento externo: " + amount + " " + currency);
}
}
// Interface esperada
public interface PaymentGateway {
void pay(BigDecimal amount);
String getCurrency();
}
// Adaptador
public class ExternalPaymentAdapter implements PaymentGateway {
private final ExternalPaymentService externalService;
private static final String CURRENCY = "USD";
public ExternalPaymentAdapter(ExternalPaymentService externalService) {
this.externalService = externalService;
}
@Override
public void pay(BigDecimal amount) {
externalService.processPayment(CURRENCY, amount.doubleValue());
}
@Override
public String getCurrency() {
return CURRENCY;
}
}
// Uso
ExternalPaymentService externalService = new ExternalPaymentService();
PaymentGateway gateway = new ExternalPaymentAdapter(externalService);
gateway.pay(new BigDecimal("100.00"));
Anti-Padrões & Boas Práticas
Quando usar padrões
✅ Use padrões quando:
- Problema de design reconhecido e testado
- Melhoram a comunicabilidade do código
- Reduzem acoplamento
❌ Não use padrões quando:
- Solução mais simples funciona
- Adiciona complexidade desnecessária
- Forçar padrão em problema errado
Princípios-chave
- Programe para interfaces, não implementações
- Prefira composição sobre herança
- Padrões resolvem problemas, não os criam
- Padrões são guias, não regras absolutas
Práticas modernas do Java
| Versão Java | Característica | Padrão afetado |
|---|---|---|
| Java 8+ | Lambdas & Streams | Strategy, Observer |
| Java 9+ | Coleções imutáveis (List.of, etc.) | Builder pode ser substituído |
| Java 14+ | Records | DTOs, Value Objects |
| Java 15+ | Text Blocks | Strings multilinha |
| Java 17+ | Pattern Matching | Visitor simplificado |
*Skill baseada no site Refactoring Guru e experiência prática de desenvolvimento Java.