Padrões de projeto: quando e como usar
1. Fundamentos dos Padrões de Projeto
Padrões de projeto são soluções reutilizáveis para problemas recorrentes no desenvolvimento de software. O conceito foi popularizado pelo livro "Design Patterns: Elements of Reusable Object-Oriented Software" (GoF — Gang of Four), que catalogou 23 padrões divididos em três categorias: criacionais, estruturais e comportamentais.
A ideia central é capturar a experiência de desenvolvedores experientes em soluções testadas e documentadas. No entanto, a aplicação inadequada pode gerar o que chamamos de "overengineering" — quando a complexidade da solução supera o problema original. O segredo está em identificar o momento certo para cada padrão.
2. Critérios para Escolher um Padrão
Antes de aplicar um padrão, é necessário analisar o problema sob três aspectos:
- Code smell: que sintoma o código apresenta? Duplicação, condicionais complexas, acoplamento excessivo?
- Contexto: qual o nível de acoplamento atual? A coesão está baixa? Precisamos de mais flexibilidade?
- Trade-offs: o padrão trará mais manutenibilidade ou apenas complexidade desnecessária?
A regra de ouro é: um padrão deve ser aplicado quando o benefício de longo prazo supera o custo imediato de implementação.
3. Padrões Criacionais — Quando e Como Usar
Singleton
O Singleton garante que uma classe tenha apenas uma instância e fornece um ponto global de acesso a ela.
// Exemplo: Logger global
class Logger {
private static instance: Logger;
private logFile: string;
private constructor() {
this.logFile = "app.log";
}
public static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
public log(message: string): void {
console.log(`[${new Date().toISOString()}] ${message}`);
}
}
// Uso
const logger = Logger.getInstance();
logger.log("Aplicação iniciada");
Quando usar: gerenciamento de pools de conexão, caches, configurações globais. Cuidado: estado global compartilhado dificulta testes e pode causar acoplamento oculto.
Factory Method
Encapsula a criação de objetos, permitindo que subclasses decidam qual classe instanciar.
// Exemplo: Fábrica de documentos
interface Document {
open(): void;
save(): void;
}
class PDFDocument implements Document {
open(): void { console.log("Abrindo PDF"); }
save(): void { console.log("Salvando PDF"); }
}
class WordDocument implements Document {
open(): void { console.log("Abrindo Word"); }
save(): void { console.log("Salvando Word"); }
}
abstract class DocumentFactory {
abstract createDocument(): Document;
public processDocument(): void {
const doc = this.createDocument();
doc.open();
doc.save();
}
}
class PDFFactory extends DocumentFactory {
createDocument(): Document {
return new PDFDocument();
}
}
Quando usar: quando o sistema precisa lidar com famílias de objetos relacionados ou quando a criação de objetos é complexa.
Builder
Separa a construção de um objeto complexo de sua representação, permitindo diferentes representações a partir do mesmo processo.
// Exemplo: Construção de pedido
class Order {
public items: string[] = [];
public discount: number = 0;
public shipping: string = "standard";
}
class OrderBuilder {
private order: Order;
constructor() {
this.order = new Order();
}
addItem(item: string): OrderBuilder {
this.order.items.push(item);
return this;
}
setDiscount(discount: number): OrderBuilder {
this.order.discount = discount;
return this;
}
setShipping(type: string): OrderBuilder {
this.order.shipping = type;
return this;
}
build(): Order {
return this.order;
}
}
// Uso
const order = new OrderBuilder()
.addItem("Notebook")
.addItem("Mouse")
.setDiscount(10)
.setShipping("express")
.build();
Quando usar: objetos com muitos parâmetros opcionais ou construções que exigem validação entre etapas.
4. Padrões Estruturais — Organizando Relações entre Classes
Adapter
Permite que interfaces incompatíveis trabalhem juntas.
// Exemplo: Integração de sistema legado
class LegacyPaymentSystem {
processPayment(amount: number): string {
return `Pagamento de R$${amount} processado (legado)`;
}
}
interface ModernPaymentGateway {
pay(amount: number): boolean;
}
class PaymentAdapter implements ModernPaymentGateway {
private legacySystem: LegacyPaymentSystem;
constructor() {
this.legacySystem = new LegacyPaymentSystem();
}
pay(amount: number): boolean {
const result = this.legacySystem.processPayment(amount);
console.log(result);
return true;
}
}
Quando usar: integração com bibliotecas de terceiros, sistemas legados ou APIs com interfaces diferentes.
Decorator
Adiciona responsabilidades a objetos dinamicamente, sem modificar sua estrutura.
// Exemplo: Notificações com recursos extras
interface Notification {
send(message: string): void;
}
class BaseNotification implements Notification {
send(message: string): void {
console.log(`Enviando: ${message}`);
}
}
class SMSDecorator implements Notification {
private wrappee: Notification;
constructor(notification: Notification) {
this.wrappee = notification;
}
send(message: string): void {
this.wrappee.send(message);
console.log("Enviando SMS com a mesma mensagem");
}
}
class EmailDecorator implements Notification {
private wrappee: Notification;
constructor(notification: Notification) {
this.wrappee = notification;
}
send(message: string): void {
this.wrappee.send(message);
console.log("Enviando e-mail com a mesma mensagem");
}
}
// Uso
let notification = new BaseNotification();
notification = new SMSDecorator(notification);
notification = new EmailDecorator(notification);
notification.send("Promoção especial!");
Quando usar: quando você precisa adicionar funcionalidades a objetos sem criar uma explosão de subclasses.
Facade
Fornece uma interface simplificada para um conjunto complexo de subsistemas.
// Exemplo: Sistema de e-commerce
class InventorySystem {
checkStock(productId: string): boolean {
console.log(`Verificando estoque do produto ${productId}`);
return true;
}
}
class PaymentSystem {
processPayment(amount: number): boolean {
console.log(`Processando pagamento de R$${amount}`);
return true;
}
}
class ShippingSystem {
scheduleDelivery(address: string): string {
console.log(`Agendando entrega para ${address}`);
return "Código de rastreio: BR123";
}
}
class EcommerceFacade {
private inventory: InventorySystem;
private payment: PaymentSystem;
private shipping: ShippingSystem;
constructor() {
this.inventory = new InventorySystem();
this.payment = new PaymentSystem();
this.shipping = new ShippingSystem();
}
placeOrder(productId: string, amount: number, address: string): string {
if (!this.inventory.checkStock(productId)) {
return "Produto fora de estoque";
}
if (!this.payment.processPayment(amount)) {
return "Falha no pagamento";
}
return this.shipping.scheduleDelivery(address);
}
}
Quando usar: para simplificar a interação com subsistemas complexos, reduzindo o acoplamento entre cliente e subsistemas.
5. Padrões Comportamentais — Gerenciando Algoritmos e Comunicação
Strategy
Permite definir uma família de algoritmos, encapsulá-los e torná-los intercambiáveis.
// Exemplo: Cálculo de frete
interface ShippingStrategy {
calculate(weight: number, distance: number): number;
}
class ExpressShipping implements ShippingStrategy {
calculate(weight: number, distance: number): number {
return weight * 0.5 + distance * 0.3;
}
}
class StandardShipping implements ShippingStrategy {
calculate(weight: number, distance: number): number {
return weight * 0.2 + distance * 0.1;
}
}
class OrderProcessor {
private strategy: ShippingStrategy;
constructor(strategy: ShippingStrategy) {
this.strategy = strategy;
}
setStrategy(strategy: ShippingStrategy): void {
this.strategy = strategy;
}
processOrder(weight: number, distance: number): number {
return this.strategy.calculate(weight, distance);
}
}
// Uso
const order = new OrderProcessor(new StandardShipping());
console.log(order.processOrder(10, 100)); // Frete padrão
order.setStrategy(new ExpressShipping());
console.log(order.processOrder(10, 100)); // Frete expresso
Quando usar: substituir condicionais complexas (if/else ou switch) por algoritmos intercambiáveis.
Observer
Define uma dependência um-para-muitos entre objetos, para que quando um objeto mude de estado, todos os dependentes sejam notificados.
// Exemplo: Sistema de eventos
interface Observer {
update(event: string, data: any): void;
}
class EventManager {
private observers: Observer[] = [];
subscribe(observer: Observer): void {
this.observers.push(observer);
}
unsubscribe(observer: Observer): void {
this.observers = this.observers.filter(o => o !== observer);
}
notify(event: string, data: any): void {
this.observers.forEach(o => o.update(event, data));
}
}
class EmailNotifier implements Observer {
update(event: string, data: any): void {
console.log(`Enviando e-mail sobre ${event}: ${JSON.stringify(data)}`);
}
}
class SMSNotifier implements Observer {
update(event: string, data: any): void {
console.log(`Enviando SMS sobre ${event}: ${JSON.stringify(data)}`);
}
}
// Uso
const manager = new EventManager();
const emailNotifier = new EmailNotifier();
const smsNotifier = new SMSNotifier();
manager.subscribe(emailNotifier);
manager.subscribe(smsNotifier);
manager.notify("order_created", { id: 123, total: 250 });
Quando usar: sistemas de eventos, notificações, MVC (Model-View-Controller) e comunicação entre componentes desacoplados.
6. Armadilhas e Antipadrões Comuns
- Padrão "porque sim": aplicar padrões sem justificativa real no domínio do problema.
- Implementação prematura: usar padrões como "solução em busca de um problema".
- Complexidade desnecessária: às vezes, um simples
if/elseé mais adequado que um Strategy completo. - Singleton abusivo: estado global compartilhado dificulta testes e manutenção.
7. Metodologia Prática para Aplicação
Passo a passo prático:
- Isole o problema: identifique o code smell específico.
- Liste padrões candidatos: quais padrões resolvem esse tipo de problema?
- Prototipe: implemente uma versão simples do padrão escolhido.
- Refatore: compare com a versão original e avalie se houve melhoria.
Exemplo prático: evolução de um sistema de pagamentos.
// Antes: switch gigante
function processPayment(type: string, amount: number) {
if (type === "credit") { /* lógica */ }
else if (type === "debit") { /* lógica */ }
else if (type === "pix") { /* lógica */ }
}
// Depois: Strategy + Factory
interface PaymentStrategy {
pay(amount: number): boolean;
}
class CreditPayment implements PaymentStrategy {
pay(amount: number): boolean {
console.log(`Crédito: R$${amount}`);
return true;
}
}
class PaymentFactory {
static createPayment(type: string): PaymentStrategy {
switch (type) {
case "credit": return new CreditPayment();
case "debit": return new DebitPayment();
default: throw new Error("Tipo inválido");
}
}
}
Checklist de validação:
- O padrão resolve o problema real?
- Reduz o acoplamento entre componentes?
- Facilita a escrita de testes unitários?
- A complexidade adicionada é justificada?
8. Padrões e Arquitetura Moderna
Os padrões de projeto se alinham diretamente com princípios como SOLID (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) e GRASP (General Responsibility Assignment Software Patterns).
Em microsserviços, padrões como Saga (para transações distribuídas) e Circuit Breaker (para resiliência) são adaptações dos padrões GoF para sistemas distribuídos.
A regra final: simplicidade é a meta. Um padrão bem aplicado reduz complexidade; um mal aplicado aumenta. Se o código ficou mais difícil de entender, repense a decisão.
Referências
- Design Patterns: Elements of Reusable Object-Oriented Software (GoF) — Livro clássico que catalogou os 23 padrões de projeto originais, base para todo o conteúdo deste artigo.
- Refactoring Guru — Design Patterns — Guia interativo com exemplos práticos em várias linguagens, incluindo explicações sobre quando e como usar cada padrão.
- SourceMaking — Design Patterns — Catálogo completo com diagramas UML, exemplos de código e discussão sobre antipadrões relacionados.
- Microsoft Docs — Design Patterns for Cloud Applications — Padrões de projeto aplicados a sistemas distribuídos e microsserviços, incluindo Saga e Circuit Breaker.
- DZone — Design Patterns: A Guide to When and How to Use Them — Artigo técnico que aborda os critérios de escolha e armadilhas comuns na aplicação de padrões de projeto.
- Martin Fowler — Patterns of Enterprise Application Architecture — Livro que expande os padrões GoF para o contexto de aplicações empresariais, com foco em arquitetura e design de sistemas.