Introdução
Padrões de projeto (Design Patterns) são soluções reutilizáveis para problemas comuns no desenvolvimento de software. Eles representam melhores práticas adotadas por desenvolvedores experientes e ajudam a criar sistemas mais flexíveis, escaláveis e de fácil manutenção.
Neste artigo, exploraremos 10 dos principais padrões de projeto, explicando:
- O problema que cada padrão resolve.
- Uma solução sem o padrão (e seus problemas).
- A solução com o padrão (implementação em Java).
- Vantagens e desvantagens de cada abordagem.
Os padrões abordados serão:
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Adapter
- Decorator
- Observer
- Strategy
- Command
1. Singleton
Problema
Garantir que uma classe tenha apenas uma instância e fornecer um ponto global de acesso a ela.
Solução sem Singleton (Problema)
public class Database {
public Database() {}
public void query(String sql) {
System.out.println("Executando: " + sql);
}
}
// Uso:
Database db1 = new Database();
Database db2 = new Database();
System.out.println(db1 == db2); // false → Diferentes instâncias!
Problema: Várias instâncias podem causar inconsistência.
Solução com Singleton
public class Database {
private static Database instance;
private Database() {} // Construtor privado
public static Database getInstance() {
if (instance == null) {
instance = new Database();
}
return instance;
}
public void query(String sql) {
System.out.println("Executando: " + sql);
}
}
// Uso:
Database db1 = Database.getInstance();
Database db2 = Database.getInstance();
System.out.println(db1 == db2); // true → Mesma instância!
Vantagens:
- Controle de instância única.
- Acesso global.
Desvantagens:
- Dificulta testes unitários.
- Pode levar a acoplamento.
2. Factory Method
Problema
Criar objetos sem especificar a classe concreta, delegando a decisão para subclasses.
Solução sem Factory Method
public class Payment {
public void process(String type) {
if (type.equals("credit")) {
System.out.println("Processando crédito");
} else if (type.equals("debit")) {
System.out.println("Processando débito");
}
// Problema: Violação do OCP (aberto/fechado)
}
}
Solução com Factory Method
public interface Payment {
void process();
}
public class CreditPayment implements Payment {
@Override
public void process() {
System.out.println("Processando crédito");
}
}
public class DebitPayment implements Payment {
@Override
public void process() {
System.out.println("Processando débito");
}
}
public abstract class PaymentFactory {
public abstract Payment createPayment();
}
public class CreditPaymentFactory extends PaymentFactory {
@Override
public Payment createPayment() {
return new CreditPayment();
}
}
// Uso:
PaymentFactory factory = new CreditPaymentFactory();
Payment payment = factory.createPayment();
payment.process();
Vantagens:
- Extensível (novos tipos sem modificar código existente).
- Baixo acoplamento.
3. Abstract Factory
Problema
Criar famílias de objetos relacionados sem especificar suas classes concretas.
Exemplo: UI (Dark vs Light Theme)
public interface Button {
void render();
}
public class DarkButton implements Button {
@Override
public void render() {
System.out.println("Dark Button");
}
}
public interface UIFactory {
Button createButton();
}
public class DarkThemeFactory implements UIFactory {
@Override
public Button createButton() {
return new DarkButton();
}
}
// Uso:
UIFactory factory = new DarkThemeFactory();
Button button = factory.createButton();
button.render();
Vantagens:
- Troca de famílias de objetos facilmente.
4. Builder
Problema
Construir objetos complexos passo a passo, evitando construtores com muitos parâmetros.
Solução sem Builder
public class Pizza {
private String dough;
private String sauce;
private String topping;
public Pizza(String dough, String sauce, String topping) {
this.dough = dough;
this.sauce = sauce;
this.topping = topping;
}
// Problema: Construtor complicado se houver muitos atributos.
}
Solução com Builder
public class Pizza {
private String dough;
private String sauce;
private String topping;
public static class Builder {
private String dough;
private String sauce;
private String topping;
public Builder dough(String dough) {
this.dough = dough;
return this;
}
// Setters similares...
public Pizza build() {
return new Pizza(this);
}
}
private Pizza(Builder builder) {
this.dough = builder.dough;
this.sauce = builder.sauce;
this.topping = builder.topping;
}
}
// Uso:
Pizza pizza = new Pizza.Builder()
.dough("fina")
.sauce("tomate")
.topping("queijo")
.build();
Vantagens:
- Flexível e legível.
- Imutabilidade.
5. Prototype
Problema
Criar novos objetos clonando uma instância existente, evitando custos de inicialização.
Exemplo em Java
public abstract class Shape implements Cloneable {
public abstract void draw();
@Override
public Shape clone() {
try {
return (Shape) super.clone();
} catch (CloneNotSupportedException e) {
return null;
}
}
}
public class Circle extends Shape {
@Override
public void draw() {
System.out.println("Desenhando círculo");
}
}
// Uso:
Shape original = new Circle();
Shape clone = original.clone();
clone.draw();
Vantagens:
- Evita recriação custosa de objetos.
6. Adapter
Problema
Permitir que interfaces incompatíveis trabalhem juntas.
Exemplo: Adaptador de Tomada
public interface EuropeanSocket {
void plugInEurope();
}
public class AmericanSocket {
public void plugInAmerica() {
System.out.println("Plugado nos EUA");
}
}
public class SocketAdapter implements EuropeanSocket {
private AmericanSocket socket;
public SocketAdapter(AmericanSocket socket) {
this.socket = socket;
}
@Override
public void plugInEurope() {
socket.plugInAmerica();
}
}
// Uso:
EuropeanSocket adapter = new SocketAdapter(new AmericanSocket());
adapter.plugInEurope();
Vantagens:
- Integração de sistemas legados.
7. Decorator
Problema
Adicionar funcionalidades dinamicamente a um objeto.
Exemplo: Café com Adicionais
public interface Coffee {
double getCost();
String getDescription();
}
public class SimpleCoffee implements Coffee {
@Override
public double getCost() { return 2.0; }
@Override
public String getDescription() { return "Café simples"; }
}
public abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}
}
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public double getCost() {
return decoratedCoffee.getCost() + 0.5;
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription() + ", com leite";
}
}
// Uso:
Coffee coffee = new MilkDecorator(new SimpleCoffee());
System.out.println(coffee.getDescription() + " - R$" + coffee.getCost());
Vantagens:
- Flexibilidade para adicionar comportamentos.
8. Observer
Problema
Notificar múltiplos objetos sobre mudanças em um estado.
Exemplo: Sistema de Notícias
public interface Observer {
void update(String news);
}
public class NewsAgency {
private List<Observer> observers = new ArrayList<>();
public void addObserver(Observer observer) {
observers.add(observer);
}
public void notifyObservers(String news) {
for (Observer observer : observers) {
observer.update(news);
}
}
}
public class NewsChannel implements Observer {
@Override
public void update(String news) {
System.out.println("Notícia recebida: " + news);
}
}
// Uso:
NewsAgency agency = new NewsAgency();
agency.addObserver(new NewsChannel());
agency.notifyObservers("Novo artigo sobre Design Patterns!");
Vantagens:
- Acoplamento fraco entre publicador e assinantes.
9. Strategy
Problema
Definir algoritmos intercambiáveis em tempo de execução.
Exemplo: Ordenação
public interface SortingStrategy {
void sort(int[] array);
}
public class BubbleSort implements SortingStrategy {
@Override
public void sort(int[] array) {
System.out.println("Ordenando com BubbleSort");
}
}
public class Context {
private SortingStrategy strategy;
public void setStrategy(SortingStrategy strategy) {
this.strategy = strategy;
}
public void executeStrategy(int[] array) {
strategy.sort(array);
}
}
// Uso:
Context context = new Context();
context.setStrategy(new BubbleSort());
context.executeStrategy(new int[]{5, 2, 9});
Vantagens:
- Troca de algoritmos sem modificar o cliente.
10. Command
Problema
Encapsular solicitações como objetos, permitindo operações como filas e desfazer.
Exemplo: Controle Remoto
public interface Command {
void execute();
}
public class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.turnOn();
}
}
public class RemoteControl {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void pressButton() {
command.execute();
}
}
// Uso:
RemoteControl remote = new RemoteControl();
remote.setCommand(new LightOnCommand(new Light()));
remote.pressButton();
Vantagens:
- Operações desacopladas.
Conclusão
Padrões de projeto são ferramentas essenciais para resolver problemas recorrentes no desenvolvimento de software. Eles promovem:
Reutilização de código
Baixo acoplamento
Flexibilidade e manutenibilidade
Este artigo cobriu 10 padrões fundamentais com exemplos em Java. A escolha do padrão certo depende do contexto do problema. Pratique e experimente para dominá-los!