Padrões de projeto: Singleton, Factory e Observer na prática

1. Introdução aos Padrões de Projeto e seu Papel na Arquitetura de Software

Padrões de projeto são soluções reutilizáveis para problemas recorrentes no desenvolvimento de software. Popularizados pelo livro "Design Patterns: Elements of Reusable Object-Oriented Software" (GoF) em 1994, os padrões surgiram da necessidade de catalogar boas práticas que arquitetos e desenvolvedores já utilizavam empiricamente.

Os benefícios são claros: reutilização de soluções testadas, melhoria na comunicação entre equipes (já que um padrão nomeado comunica uma ideia complexa em uma única palavra) e redução significativa de retrabalho. Em uma lista de 1200 temas de engenharia de software, Singleton, Factory e Observer ocupam posições centrais por resolverem problemas fundamentais de criação, estruturação e comunicação entre objetos.

2. Singleton: Garantindo uma Única Instância Global

O padrão Singleton resolve o problema de garantir que uma classe tenha apenas uma instância, fornecendo um ponto de acesso global a ela. É ideal para recursos compartilhados como conexões de banco de dados, sistemas de logging e configurações de aplicação.

Implementação prática:

public class Logger {
    private static Logger instancia;

    private Logger() {
        // Construtor privado impede instanciação externa
    }

    public static Logger getInstance() {
        if (instancia == null) {
            instancia = new Logger();
        }
        return instancia;
    }

    public void log(String mensagem) {
        System.out.println("[LOG] " + mensagem);
    }
}

// Uso
Logger logger = Logger.getInstance();
logger.log("Sistema iniciado");

Cuidados: Singleton dificulta testes unitários (cria acoplamento global) e, em ambientes concorrentes, exige sincronização no método getInstance(). Prefira injeção de dependência quando possível.

3. Factory: Delegando a Criação de Objetos Complexos

O padrão Factory resolve o problema de quando a lógica de instanciação é complexa ou varia em tempo de execução. Existem duas variações principais: Factory Method (um método que cria objetos) e Abstract Factory (uma interface para criar famílias de objetos relacionados).

Exemplo concreto — Sistema de Notificações:

// Interface do produto
interface Notificacao {
    void enviar(String mensagem);
}

// Produtos concretos
class EmailNotificacao implements Notificacao {
    public void enviar(String mensagem) {
        System.out.println("Enviando email: " + mensagem);
    }
}

class SMSNotificacao implements Notificacao {
    public void enviar(String mensagem) {
        System.out.println("Enviando SMS: " + mensagem);
    }
}

class PushNotificacao implements Notificacao {
    public void enviar(String mensagem) {
        System.out.println("Enviando push: " + mensagem);
    }
}

// Factory Method
class NotificacaoFactory {
    public static Notificacao criar(String tipo) {
        switch (tipo.toLowerCase()) {
            case "email": return new EmailNotificacao();
            case "sms": return new SMSNotificacao();
            case "push": return new PushNotificacao();
            default: throw new IllegalArgumentException("Tipo desconhecido");
        }
    }
}

// Uso
Notificacao notif = NotificacaoFactory.criar("email");
notif.enviar("Bem-vindo ao sistema!");

O cliente não precisa conhecer as classes concretas — apenas a interface Notificacao e a factory.

4. Observer: Comunicação Desacoplada entre Objetos

Observer define uma dependência um-para-muitos entre objetos, de modo que quando um objeto muda de estado, todos os seus dependentes são notificados automaticamente. É fundamental para sistemas de eventos, listeners e arquiteturas reativas.

Exemplo prático — Sistema de E-commerce:

import java.util.ArrayList;
import java.util.List;

// Subject (Observável)
interface EstoqueSubject {
    void registrarObserver(EstoqueObserver observer);
    void removerObserver(EstoqueObserver observer);
    void notificarObservers();
}

// Observer
interface EstoqueObserver {
    void atualizar(String produto, int quantidade);
}

// Subject concreto
class Estoque implements EstoqueSubject {
    private List<EstoqueObserver> observers = new ArrayList<>();
    private String produto;
    private int quantidade;

    public void setProduto(String produto, int quantidade) {
        this.produto = produto;
        this.quantidade = quantidade;
        notificarObservers();
    }

    public void registrarObserver(EstoqueObserver observer) {
        observers.add(observer);
    }

    public void removerObserver(EstoqueObserver observer) {
        observers.remove(observer);
    }

    public void notificarObservers() {
        for (EstoqueObserver observer : observers) {
            observer.atualizar(produto, quantidade);
        }
    }
}

// Observers concretos
class FinanceiroObserver implements EstoqueObserver {
    public void atualizar(String produto, int quantidade) {
        System.out.println("Financeiro: Atualizar custos do produto " + produto);
    }
}

class NotificacaoObserver implements EstoqueObserver {
    public void atualizar(String produto, int quantidade) {
        if (quantidade < 10) {
            System.out.println("Notificação: Estoque baixo de " + produto);
        }
    }
}

// Uso
Estoque estoque = new Estoque();
estoque.registrarObserver(new FinanceiroObserver());
estoque.registrarObserver(new NotificacaoObserver());
estoque.setProduto("Notebook", 5);

5. Comparação Prática: Quando Usar Cada Padrão

Característica Singleton Factory Observer
Propósito Instância única global Criação encapsulada Notificação desacoplada
Acoplamento Alto (acesso global) Baixo (cliente conhece interface) Baixo (apenas interface)
Testabilidade Baixa (estado global) Alta (fácil mockar) Média (cuidado com vazamentos)
Desempenho overhead mínimo overhead de criação pode ser custoso em cascata

6. Exemplo Integrado: Sistema de Pedidos

// Singleton: Logger central
class LoggerSistema {
    private static LoggerSistema instancia;
    private LoggerSistema() {}
    public static LoggerSistema getInstance() {
        if (instancia == null) instancia = new LoggerSistema();
        return instancia;
    }
    public void log(String msg) { System.out.println("[LOG] " + msg); }
}

// Interface do produto
interface Pedido {
    void processar();
}

// Produtos concretos
class PedidoPresencial implements Pedido {
    public void processar() {
        LoggerSistema.getInstance().log("Pedido presencial processado");
    }
}

class PedidoOnline implements Pedido {
    public void processar() {
        LoggerSistema.getInstance().log("Pedido online processado");
    }
}

// Factory
class PedidoFactory {
    public static Pedido criar(String tipo) {
        switch (tipo) {
            case "presencial": return new PedidoPresencial();
            case "online": return new PedidoOnline();
            default: throw new IllegalArgumentException("Tipo inválido");
        }
    }
}

// Observer
interface PedidoObserver {
    void notificar(Pedido pedido);
}

class EstoqueObserver implements PedidoObserver {
    public void notificar(Pedido pedido) {
        LoggerSistema.getInstance().log("Estoque atualizado para pedido");
    }
}

class ClienteObserver implements PedidoObserver {
    public void notificar(Pedido pedido) {
        LoggerSistema.getInstance().log("Cliente notificado sobre pedido");
    }
}

// Subject
class PedidoSubject {
    private List<PedidoObserver> observers = new ArrayList<>();
    public void registrar(PedidoObserver obs) { observers.add(obs); }
    public void notificar(Pedido pedido) {
        for (PedidoObserver obs : observers) obs.notificar(pedido);
    }
}

// Uso integrado
public class SistemaPedidos {
    public static void main(String[] args) {
        PedidoSubject subject = new PedidoSubject();
        subject.registrar(new EstoqueObserver());
        subject.registrar(new ClienteObserver());

        Pedido pedido = PedidoFactory.criar("online");
        pedido.processar();
        subject.notificar(pedido);
    }
}

7. Boas Práticas e Armadilhas Comuns

Singleton: Evite uso excessivo — cada Singleton é um ponto de acoplamento global. Prefira frameworks de injeção de dependência (como Spring) que gerenciam o escopo das instâncias.

Factory: Não crie "fábricas de tudo". Mantenha o foco em um domínio específico. Uma factory para criar conexões de banco é útil; uma factory que cria qualquer objeto do sistema é um anti-padrão.

Observer: Cuidado com vazamentos de memória — sempre remova observers quando não forem mais necessários. Em sistemas com muitos observers, a notificação em cascata pode degradar o desempenho.

Documente a escolha dos padrões: explique no código e na documentação por que determinado padrão foi escolhido, facilitando a manutenção futura.

8. Conclusão e Próximos Passos na Lista de 1200 Temas

Singleton, Factory e Observer são padrões fundamentais que resolvem problemas distintos e complementares: Singleton gerencia recursos únicos, Factory encapsula criação complexa e Observer promove comunicação desacoplada. Dominá-los é essencial para qualquer desenvolvedor que busca escrever código mais limpo, reutilizável e de fácil manutenção.

Como próximos passos, explore temas vizinhos como SOLID (especialmente o Princípio da Responsabilidade Única e Inversão de Dependência), dívida técnica (como padrões mal aplicados geram dívida) e convention over configuration (como frameworks modernos reduzem a necessidade de padrões explícitos). Pratique refatorando um código legado — identifique onde Singleton, Factory ou Observer podem simplificar a arquitetura.

Referências