Introdução aos Padrões de Projeto - Padrões de Design Comumente Usados

17 min leitura | 2024.12.27

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