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
- Design Patterns: Elements of Reusable Object-Oriented Software (GoF) — Livro clássico que introduziu os 23 padrões de design originais, base fundamental para o tema.
- Refactoring Guru - Design Patterns — Guia completo com exemplos em várias linguagens, incluindo explicações detalhadas de cada padrão.
- MDN Web Docs - JavaScript Design Patterns — Documentação oficial da Mozilla com implementações práticas de padrões em JavaScript.
- Martin Fowler - Patterns of Enterprise Application Architecture — Livro que expande padrões para aplicações empresariais, incluindo Repository e CQRS.
- Microsoft - Cloud Design Patterns — Padrões para arquiteturas em nuvem, incluindo Circuit Breaker, API Gateway e Saga.
- Node.js Design Patterns — Livro focado em padrões para Node.js, com exemplos práticos para aplicações modernas.
- React Design Patterns — Guia de padrões específicos para React, incluindo HOCs, Render Props e Hooks.