O que são Padrões de Projeto
Padrões de projeto são soluções reutilizáveis para problemas comuns no design de software. Foram sistematizados no livro do GoF (Gang of Four) em 1994.
Por que aprender padrões: Para evitar reinventar a roda e ter um vocabulário comum entre desenvolvedores.
Padrões de Criação
Singleton
Garante que uma classe tenha apenas uma instância.
// Singleton em JavaScript
class Database {
static #instance = null;
constructor() {
if (Database.#instance) {
return Database.#instance;
}
this.connection = this.connect();
Database.#instance = this;
}
connect() {
console.log('Database connected');
return { /* connection */ };
}
static getInstance() {
if (!Database.#instance) {
Database.#instance = new Database();
}
return Database.#instance;
}
}
// Uso
const db1 = Database.getInstance();
const db2 = Database.getInstance();
console.log(db1 === db2); // true
Casos de uso: Conexão com banco de dados, funcionalidade de log, gerenciamento de configurações
Factory
Encapsula a criação de objetos.
// Factory para criar notificações
class NotificationFactory {
static create(type, message) {
switch (type) {
case 'email':
return new EmailNotification(message);
case 'sms':
return new SMSNotification(message);
case 'push':
return new PushNotification(message);
default:
throw new Error(`Unknown notification type: ${type}`);
}
}
}
// Uso
const notification = NotificationFactory.create('email', 'Hello!');
notification.send();
Casos de uso: Criação de objetos condicionais, separação de dependências
Builder
Constrói objetos complexos de forma gradual.
class QueryBuilder {
constructor() {
this.query = { select: '*', from: '', where: [], orderBy: '' };
}
select(fields) {
this.query.select = fields;
return this;
}
from(table) {
this.query.from = table;
return this;
}
where(condition) {
this.query.where.push(condition);
return this;
}
orderBy(field) {
this.query.orderBy = field;
return this;
}
build() {
let sql = `SELECT ${this.query.select} FROM ${this.query.from}`;
if (this.query.where.length > 0) {
sql += ` WHERE ${this.query.where.join(' AND ')}`;
}
if (this.query.orderBy) {
sql += ` ORDER BY ${this.query.orderBy}`;
}
return sql;
}
}
// Uso
const query = new QueryBuilder()
.select('name, email')
.from('users')
.where('status = "active"')
.where('age > 18')
.orderBy('created_at DESC')
.build();
Casos de uso: Construção de queries SQL, construção de requisições HTTP, objetos de configuração complexos
Padrões Estruturais
Adapter
Converte interfaces incompatíveis.
// API antiga
class OldPaymentSystem {
processPayment(amount) {
console.log(`Old system: Processing ${amount}`);
return { success: true };
}
}
// Interface da nova API
class PaymentAdapter {
constructor(oldSystem) {
this.oldSystem = oldSystem;
}
pay(request) {
// Converte a nova interface para o sistema antigo
const result = this.oldSystem.processPayment(request.amount);
return {
transactionId: `txn_${Date.now()}`,
status: result.success ? 'completed' : 'failed'
};
}
}
// Uso
const adapter = new PaymentAdapter(new OldPaymentSystem());
adapter.pay({ amount: 1000, currency: 'JPY' });
Casos de uso: Integração com sistemas legados, abstração de bibliotecas de terceiros
Decorator
Adiciona funcionalidades dinamicamente a objetos.
// Café básico
class Coffee {
cost() { return 300; }
description() { return 'Café'; }
}
// Decorators
class MilkDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() { return this.coffee.cost() + 50; }
description() { return `${this.coffee.description()} + Leite`; }
}
class SugarDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() { return this.coffee.cost() + 20; }
description() { return `${this.coffee.description()} + Açúcar`; }
}
// Uso
let coffee = new Coffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
console.log(coffee.description()); // Café + Leite + Açúcar
console.log(coffee.cost()); // 370
Casos de uso: Middleware, adição de logs, adição de cache
Facade
Fornece uma interface simples para subsistemas complexos.
// Subsistemas complexos
class VideoDecoder { decode(file) { /* ... */ } }
class AudioDecoder { decode(file) { /* ... */ } }
class SubtitleParser { parse(file) { /* ... */ } }
class VideoPlayer { play(video, audio, subtitle) { /* ... */ } }
// Facade
class MediaPlayerFacade {
constructor() {
this.videoDecoder = new VideoDecoder();
this.audioDecoder = new AudioDecoder();
this.subtitleParser = new SubtitleParser();
this.player = new VideoPlayer();
}
playVideo(filename) {
const video = this.videoDecoder.decode(filename);
const audio = this.audioDecoder.decode(filename);
const subtitle = this.subtitleParser.parse(filename);
this.player.play(video, audio, subtitle);
}
}
// Uso (simples!)
const player = new MediaPlayerFacade();
player.playVideo('movie.mp4');
Casos de uso: Wrappers de bibliotecas, simplificação de processamentos complexos
Padrões Comportamentais
Observer
Notifica múltiplos objetos sobre mudanças de estado de um objeto.
class EventEmitter {
constructor() {
this.listeners = {};
}
on(event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
}
emit(event, data) {
if (this.listeners[event]) {
this.listeners[event].forEach(callback => callback(data));
}
}
}
// Uso
const store = new EventEmitter();
store.on('userLoggedIn', (user) => {
console.log(`Welcome, ${user.name}!`);
});
store.on('userLoggedIn', (user) => {
analytics.track('login', { userId: user.id });
});
store.emit('userLoggedIn', { id: 1, name: 'Alice' });
Casos de uso: Sistemas de eventos, gerenciamento de estado, programação reativa
Strategy
Torna algoritmos intercambiáveis.
// Estratégias de pagamento
const paymentStrategies = {
creditCard: (amount) => {
console.log(`Credit card payment: ${amount}`);
return { method: 'creditCard', fee: amount * 0.03 };
},
bankTransfer: (amount) => {
console.log(`Bank transfer: ${amount}`);
return { method: 'bankTransfer', fee: 0 };
},
paypal: (amount) => {
console.log(`PayPal payment: ${amount}`);
return { method: 'paypal', fee: amount * 0.04 };
}
};
class PaymentProcessor {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
pay(amount) {
return this.strategy(amount);
}
}
// Uso
const processor = new PaymentProcessor(paymentStrategies.creditCard);
processor.pay(1000);
processor.setStrategy(paymentStrategies.bankTransfer);
processor.pay(1000);
Casos de uso: Algoritmos de ordenação, troca de métodos de autenticação, cálculo de tarifas
Command
Encapsula operações como objetos.
// Interface de comando
class Command {
execute() { throw new Error('Not implemented'); }
undo() { throw new Error('Not implemented'); }
}
// Comando concreto
class AddTextCommand extends Command {
constructor(editor, text) {
super();
this.editor = editor;
this.text = text;
}
execute() {
this.editor.content += this.text;
}
undo() {
this.editor.content = this.editor.content.slice(0, -this.text.length);
}
}
// Executor
class CommandExecutor {
constructor() {
this.history = [];
}
execute(command) {
command.execute();
this.history.push(command);
}
undo() {
const command = this.history.pop();
if (command) {
command.undo();
}
}
}
// Uso
const editor = { content: '' };
const executor = new CommandExecutor();
executor.execute(new AddTextCommand(editor, 'Hello '));
executor.execute(new AddTextCommand(editor, 'World'));
console.log(editor.content); // 'Hello World'
executor.undo();
console.log(editor.content); // 'Hello '
Casos de uso: Funcionalidade Undo/Redo, transações, filas de tarefas
Resumo
Os padrões de projeto funcionam como uma linguagem comum no design de software, fornecendo soluções comprovadas para problemas comuns. No entanto, aplicar padrões não é um fim em si mesmo - o importante é escolher o padrão adequado para cada problema.
← Voltar para a lista