Padrões de Projeto (Design Patterns) em Java: Guia Completo com Exemplos

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:

  1. Singleton
  2. Factory Method
  3. Abstract Factory
  4. Builder
  5. Prototype
  6. Adapter
  7. Decorator
  8. Observer
  9. Strategy
  10. 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!

Deixe um comentário