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
- Design Patterns: Elements of Reusable Object-Oriented Software (GoF) — Livro clássico que catalogou os 23 padrões de projeto originais, incluindo Singleton, Factory Method e Observer
- Refactoring Guru — Singleton Pattern — Tutorial completo com explicações, diagramas e exemplos em múltiplas linguagens
- Refactoring Guru — Factory Method Pattern — Guia detalhado sobre Factory Method e Abstract Factory com exemplos práticos
- Refactoring Guru — Observer Pattern — Explicação do padrão Observer com exemplos de implementação em Java, Python e outras linguagens
- SourceMaking — Design Patterns — Repositório de padrões de projeto com descrições, motivações e exemplos de código
- Wikipedia — Observer Pattern — Definição formal, estrutura e exemplos do padrão Observer na engenharia de software
- Baeldung — Singleton Pattern in Java — Tutorial prático sobre implementação de Singleton em Java, incluindo versões thread-safe