Padrões de design mais usados em aplicações modernas

1. Introdução aos Padrões de Design no Contexto Moderno

Padrões de design são soluções reutilizáveis para problemas recorrentes no desenvolvimento de software. Originados no livro "Design Patterns: Elements of Reusable Object-Oriented Software" (GoF) em 1994, esses padrões evoluíram para se adaptar a arquiteturas modernas como microsserviços, serverless e frameworks reativos.

A relevância dos padrões de design persiste porque eles oferecem um vocabulário comum entre desenvolvedores, reduzem a dívida técnica e melhoram a manutenibilidade do código. Em aplicações modernas, padrões como Observer, Strategy e Command são fundamentais para lidar com complexidade crescente, enquanto padrões como Singleton precisam ser repensados em contextos de módulos ES6 e state management.

2. Padrões Criacionais Adaptados para Frameworks Atuais

Factory Method e Abstract Factory

Em frameworks como Angular e Spring, o Factory Method é amplamente utilizado na injeção de dependência. Considere um exemplo de fábrica de componentes em React:

// Factory Method para criação de componentes
function createButton(type, config) {
  switch(type) {
    case 'primary':
      return new PrimaryButton(config);
    case 'secondary':
      return new SecondaryButton(config);
    case 'danger':
      return new DangerButton(config);
    default:
      throw new Error('Tipo de botão não suportado');
  }
}

Singleton

O Singleton ainda é útil para gerenciamento de estado global, mas módulos ES6 oferecem alternativa mais segura:

// Singleton moderno com módulo ES6
let instance = null;

class DatabaseConnection {
  constructor() {
    if (instance) return instance;
    this.connection = this.createConnection();
    instance = this;
  }

  createConnection() {
    return { host: 'localhost', port: 5432 };
  }
}

export default new DatabaseConnection();

Builder

O padrão Builder é ideal para construção de queries ORM e objetos de configuração complexos:

// Builder para queries TypeORM
class QueryBuilder {
  constructor() {
    this.query = {};
  }

  select(fields) {
    this.query.select = fields;
    return this;
  }

  from(table) {
    this.query.from = table;
    return this;
  }

  where(condition) {
    this.query.where = condition;
    return this;
  }

  build() {
    return this.query;
  }
}

const query = new QueryBuilder()
  .select(['id', 'name'])
  .from('users')
  .where('active = true')
  .build();

3. Padrões Estruturais para Organização de Código

Adapter

O Adapter é essencial para integrar APIs legadas com novos serviços:

// Adapter para API legada
class LegacyPaymentAPI {
  processPayment(amount, currency) {
    return `Processing ${amount} ${currency}`;
  }
}

class ModernPaymentAdapter {
  constructor(legacyAPI) {
    this.legacyAPI = legacyAPI;
  }

  pay(amount, currency) {
    const convertedAmount = this.convertCurrency(amount, currency);
    return this.legacyAPI.processPayment(convertedAmount, 'USD');
  }

  convertCurrency(amount, currency) {
    // Lógica de conversão
    return amount * 1.2;
  }
}

Decorator

Em frameworks como Express, middlewares implementam o Decorator:

// Decorator como middleware Express
function loggerMiddleware(req, res, next) {
  console.log(`${req.method} ${req.url}`);
  next();
}

function authMiddleware(req, res, next) {
  if (req.headers.authorization) {
    next();
  } else {
    res.status(401).send('Unauthorized');
  }
}

app.use(loggerMiddleware);
app.use(authMiddleware);

Composite

O Composite é utilizado em árvores de componentes UI:

// Composite para componentes React
class Component {
  constructor(name) {
    this.name = name;
    this.children = [];
  }

  add(child) {
    this.children.push(child);
  }

  render() {
    return `<div>${this.name}${this.children.map(c => c.render()).join('')}</div>`;
  }
}

const root = new Component('App');
const header = new Component('Header');
const content = new Component('Content');
root.add(header);
root.add(content);

4. Padrões Comportamentais em Aplicações Reativas

Observer

O padrão Observer é a base de sistemas Pub/Sub e hooks de frameworks:

// Observer para sistema de eventos
class EventBus {
  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));
    }
  }
}

const bus = new EventBus();
bus.on('userLogin', (user) => console.log(`${user} logged in`));
bus.emit('userLogin', 'John');

Strategy

O Strategy substitui condicionais complexos por algoritmos plugáveis:

// Strategy para validação de formulários
class Validator {
  constructor(strategies) {
    this.strategies = strategies;
  }

  validate(field, value) {
    const strategy = this.strategies[field];
    if (strategy) {
      return strategy(value);
    }
    return true;
  }
}

const strategies = {
  email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
  password: (value) => value.length >= 8,
  age: (value) => value >= 18 && value <= 120
};

const validator = new Validator(strategies);
console.log(validator.validate('email', 'test@example.com')); // true

Command

O Command é utilizado em filas de tarefas e undo/redo:

// Command para operações assíncronas
class Command {
  constructor(execute, undo) {
    this.execute = execute;
    this.undo = undo;
  }
}

class CommandQueue {
  constructor() {
    this.queue = [];
    this.history = [];
  }

  add(command) {
    this.queue.push(command);
  }

  executeAll() {
    this.queue.forEach(command => {
      command.execute();
      this.history.push(command);
    });
    this.queue = [];
  }

  undoLast() {
    const command = this.history.pop();
    if (command) {
      command.undo();
    }
  }
}

5. Padrões Modernos para Gerenciamento de Estado e Dados

Redux Pattern (Flux)

O Redux gerencia estado previsível em front-ends complexos:

// Redux Pattern simplificado
class Store {
  constructor(reducer, initialState) {
    this.reducer = reducer;
    this.state = initialState;
    this.listeners = [];
  }

  dispatch(action) {
    this.state = this.reducer(this.state, action);
    this.listeners.forEach(listener => listener(this.state));
  }

  subscribe(listener) {
    this.listeners.push(listener);
  }

  getState() {
    return this.state;
  }
}

const reducer = (state = { count: 0 }, action) => {
  switch(action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 };
    case 'DECREMENT':
      return { ...state, count: state.count - 1 };
    default:
      return state;
  }
};

const store = new Store(reducer, { count: 0 });
store.dispatch({ type: 'INCREMENT' });

Repository Pattern

O Repository abstrai acesso a dados em back-ends:

// Repository Pattern com Prisma
class UserRepository {
  constructor(prisma) {
    this.prisma = prisma;
  }

  async findById(id) {
    return this.prisma.user.findUnique({ where: { id } });
  }

  async create(data) {
    return this.prisma.user.create({ data });
  }

  async update(id, data) {
    return this.prisma.user.update({ where: { id }, data });
  }

  async delete(id) {
    return this.prisma.user.delete({ where: { id } });
  }
}

CQRS

CQRS separa leitura e escrita em sistemas de alta performance:

// CQRS simplificado
class CommandHandler {
  async handleCreateUser(command) {
    // Lógica de escrita
    return { id: 1, name: command.name };
  }
}

class QueryHandler {
  async handleGetUser(query) {
    // Lógica de leitura
    return { id: query.id, name: 'John' };
  }
}

6. Padrões de Integração entre Serviços

Circuit Breaker

O Circuit Breaker garante resiliência em chamadas de rede:

// Circuit Breaker com Opossum
const circuitBreaker = require('opossum');

async function callExternalAPI() {
  const response = await fetch('https://api.example.com/data');
  return response.json();
}

const breaker = new circuitBreaker(callExternalAPI, {
  timeout: 3000,
  errorThresholdPercentage: 50,
  resetTimeout: 30000
});

breaker.fallback(() => ({ error: 'Serviço indisponível' }));
breaker.fire().then(console.log).catch(console.error);

API Gateway

O API Gateway centraliza a entrada para microsserviços:

// API Gateway simplificado
class APIGateway {
  constructor() {
    this.routes = {};
  }

  register(service, handler) {
    this.routes[service] = handler;
  }

  async handleRequest(request) {
    const service = this.routes[request.service];
    if (service) {
      return service(request);
    }
    return { error: 'Service not found' };
  }
}

Saga Pattern

O Saga coordena transações distribuídas:

// Saga Pattern para transações distribuídas
class Saga {
  constructor() {
    this.steps = [];
    this.compensations = [];
  }

  addStep(execute, compensate) {
    this.steps.push(execute);
    this.compensations.push(compensate);
  }

  async execute() {
    for (let i = 0; i < this.steps.length; i++) {
      try {
        await this.steps[i]();
      } catch (error) {
        for (let j = i - 1; j >= 0; j--) {
          await this.compensations[j]();
        }
        throw error;
      }
    }
  }
}

7. Boas Práticas e Armadilhas Comuns

Quando evitar padrões

Overengineering ocorre quando padrões são aplicados desnecessariamente. Para projetos pequenos, soluções simples são mais eficientes. Avalie se o padrão realmente resolve um problema recorrente.

Combinação de padrões

Compor múltiplos padrões é comum, mas exige cuidado. Por exemplo, Repository com Factory Method ou Command com Observer podem coexistir, desde que a responsabilidade de cada um seja clara.

Documentação e testes

Documente os padrões utilizados e crie testes unitários para garantir que a equipe compreenda as abstrações. Use exemplos claros no código e mantenha uma wiki ou README com explicações.


Referências