Design Patterns (Padrões de Projeto)

Design Patterns (Padrões de Projeto)

Ponto chave: Patterns são vocabulário compartilhado, não dogma. O GoF catalogou 23 patterns em 1994 — você precisa dominar uns 10 e saber quando não usá-los.

O que são Design Patterns

Em 1994, Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides (o “Gang of Four”) publicaram Design Patterns: Elements of Reusable Object-Oriented Software. O livro catalogou 23 soluções recorrentes para problemas comuns em design orientado a objetos, divididas em três categorias:

  • Creational — controlam a criação de objetos
  • Structural — definem como objetos se compõem em estruturas maiores
  • Behavioral — definem como objetos colaboram e distribuem responsabilidades

A ideia central: você não precisa reinventar soluções para problemas que já foram resolvidos milhares de vezes. Patterns são um vocabulário compartilhado entre engenheiros — quando alguém diz “isso é um Observer”, toda a equipe entende a estrutura, os trade-offs e as implicações.


Creational Patterns

Singleton

O Singleton garante que uma classe tenha exatamente uma instância e fornece um ponto de acesso global a ela. Casos clássicos: pool de conexões com banco de dados, logger centralizado, configuração da aplicação.

// Singleton com proteção no construtor — TypeScript
class DatabasePool {
  private static instance: DatabasePool | null = null;
  private connections: Map<string, unknown> = new Map();

  // O construtor privado impede `new DatabasePool()` externo
  private constructor(private readonly maxConnections: number) {}

  static getInstance(maxConnections = 10): DatabasePool {
    if (!DatabasePool.instance) {
      DatabasePool.instance = new DatabasePool(maxConnections);
    }
    return DatabasePool.instance;
  }

  getConnection(id: string): unknown {
    if (!this.connections.has(id) && this.connections.size >= this.maxConnections) {
      throw new Error(`Pool esgotado: máximo de ${this.maxConnections} conexões`);
    }
    // Lógica de conexão real aqui
    return this.connections.get(id);
  }

  // CRÍTICO para testes: permite resetar a instância
  static resetInstance(): void {
    DatabasePool.instance = null;
  }
}

const pool1 = DatabasePool.getInstance();
const pool2 = DatabasePool.getInstance();
console.log(pool1 === pool2); // true — mesma instância

Problema do Singleton com testes: Singletons introduzem estado global compartilhado. Em testes paralelos, um teste pode corromper o estado que outro teste depende. A alternativa moderna em JavaScript é o Module Pattern — o sistema de módulos do Node.js já faz cache por padrão:

// module-singleton.ts — O módulo inteiro é avaliado UMA vez
// Node.js cacheia o resultado; todo `import` retorna a mesma referência.

const config = {
  dbUrl: process.env.DATABASE_URL ?? "postgres://localhost:5432/app",
  redisUrl: process.env.REDIS_URL ?? "redis://localhost:6379",
  maxRetries: 3,
} as const;

// Cada arquivo que importar `config` recebe o MESMO objeto
export default config;

Factory Method / Abstract Factory

O Factory Method define uma interface para criar objetos, mas delega a decisão de qual classe concreta instanciar para subclasses ou funções. Isso implementa o Dependency Inversion Principle — código de alto nível depende de abstrações, não de implementações concretas.

// Definição das abstrações
interface Logger {
  log(message: string, context?: Record<string, unknown>): void;
  error(message: string, error?: Error): void;
}

interface LoggerFactory {
  createLogger(namespace: string): Logger;
}

// Implementação concreta: Console
class ConsoleLogger implements Logger {
  constructor(private readonly namespace: string) {}

  log(message: string, context?: Record<string, unknown>): void {
    console.log(`[${this.namespace}] ${message}`, context ?? "");
  }

  error(message: string, error?: Error): void {
    console.error(`[${this.namespace}] ERROR: ${message}`, error?.stack ?? "");
  }
}

// Implementação concreta: JSON estruturado (produção)
class StructuredLogger implements Logger {
  constructor(private readonly namespace: string) {}

  log(message: string, context?: Record<string, unknown>): void {
    const entry = {
      level: "info",
      namespace: this.namespace,
      message,
      timestamp: new Date().toISOString(),
      ...context,
    };
    process.stdout.write(JSON.stringify(entry) + "\n");
  }

  error(message: string, error?: Error): void {
    const entry = {
      level: "error",
      namespace: this.namespace,
      message,
      stack: error?.stack,
      timestamp: new Date().toISOString(),
    };
    process.stderr.write(JSON.stringify(entry) + "\n");
  }
}

// Factory que decide a implementação baseado no ambiente
class LoggerFactoryImpl implements LoggerFactory {
  createLogger(namespace: string): Logger {
    if (process.env.NODE_ENV === "production") {
      return new StructuredLogger(namespace);
    }
    return new ConsoleLogger(namespace);
  }
}

// Uso — o código consumidor NUNCA sabe qual implementação está usando
const factory: LoggerFactory = new LoggerFactoryImpl();
const logger = factory.createLogger("UserService");
logger.log("Usuário criado", { userId: "abc-123" });

Abstract Factory estende o conceito: em vez de criar um único tipo de objeto, cria famílias de objetos relacionados. Exemplo clássico: um toolkit de UI que cria botões, inputs e modais com estilos diferentes (Material, Bootstrap) sem que o código consumidor saiba qual família está sendo usada.

Builder

O Builder separa a construção de um objeto complexo da sua representação. Útil quando um objeto tem muitos parâmetros opcionais — evita construtores com 15 argumentos (telescoping constructor anti-pattern).

interface QueryConfig {
  table: string;
  fields: string[];
  conditions: string[];
  orderBy?: { field: string; direction: "ASC" | "DESC" };
  limit?: number;
  offset?: number;
  joins: string[];
}

class QueryBuilder {
  private config: QueryConfig;

  constructor(table: string) {
    this.config = { table, fields: [], conditions: [], joins: [] };
  }

  select(...fields: string[]): this {
    this.config.fields.push(...fields);
    return this; // Retorna `this` para permitir fluent interface
  }

  where(condition: string): this {
    this.config.conditions.push(condition);
    return this;
  }

  join(table: string, on: string): this {
    this.config.joins.push(`JOIN ${table} ON ${on}`);
    return this;
  }

  orderBy(field: string, direction: "ASC" | "DESC" = "ASC"): this {
    this.config.orderBy = { field, direction };
    return this;
  }

  limit(n: number): this {
    this.config.limit = n;
    return this;
  }

  offset(n: number): this {
    this.config.offset = n;
    return this;
  }

  build(): string {
    const fields = this.config.fields.length > 0
      ? this.config.fields.join(", ")
      : "*";

    let sql = `SELECT ${fields} FROM ${this.config.table}`;

    if (this.config.joins.length > 0) {
      sql += ` ${this.config.joins.join(" ")}`;
    }
    if (this.config.conditions.length > 0) {
      sql += ` WHERE ${this.config.conditions.join(" AND ")}`;
    }
    if (this.config.orderBy) {
      sql += ` ORDER BY ${this.config.orderBy.field} ${this.config.orderBy.direction}`;
    }
    if (this.config.limit !== undefined) {
      sql += ` LIMIT ${this.config.limit}`;
    }
    if (this.config.offset !== undefined) {
      sql += ` OFFSET ${this.config.offset}`;
    }

    return sql;
  }
}

// Fluent interface — lê quase como linguagem natural
const query = new QueryBuilder("users")
  .select("id", "name", "email")
  .join("orders", "orders.user_id = users.id")
  .where("users.active = true")
  .where("orders.total > 100")
  .orderBy("users.name", "ASC")
  .limit(20)
  .offset(40)
  .build();

// SELECT id, name, email FROM users JOIN orders ON orders.user_id = users.id
// WHERE users.active = true AND orders.total > 100 ORDER BY users.name ASC LIMIT 20 OFFSET 40

Structural Patterns

Adapter

O Adapter faz interfaces incompatíveis trabalharem juntas. É um wrapper que traduz a interface de uma classe para a interface que o cliente espera. Essencial quando você integra bibliotecas de terceiros ou migra de uma API legada.

// Interface que nosso sistema espera
interface PaymentGateway {
  charge(amount: number, currency: string, token: string): Promise<{ id: string; status: string }>;
  refund(transactionId: string, amount: number): Promise<{ success: boolean }>;
}

// API legada de terceiro com interface completamente diferente
class LegacyStripeClient {
  async createCharge(params: {
    amount_cents: number;
    currency_code: string;
    source_token: string;
  }): Promise<{ charge_id: string; charge_status: string }> {
    // Chamada real à API do Stripe v1
    return { charge_id: "ch_xxx", charge_status: "succeeded" };
  }

  async reverseCharge(chargeId: string, refundAmountCents: number): Promise<{ reversed: boolean }> {
    return { reversed: true };
  }
}

// Adapter: traduz nossa interface para a API legada
class StripeAdapter implements PaymentGateway {
  constructor(private readonly client: LegacyStripeClient) {}

  async charge(amount: number, currency: string, token: string) {
    const result = await this.client.createCharge({
      amount_cents: Math.round(amount * 100), // Converte reais para centavos
      currency_code: currency.toUpperCase(),
      source_token: token,
    });

    return {
      id: result.charge_id,
      status: result.charge_status,
    };
  }

  async refund(transactionId: string, amount: number) {
    const result = await this.client.reverseCharge(
      transactionId,
      Math.round(amount * 100)
    );
    return { success: result.reversed };
  }
}

// O serviço de pagamento depende APENAS da interface — não da implementação
class PaymentService {
  constructor(private readonly gateway: PaymentGateway) {}

  async processOrder(orderId: string, amount: number, token: string) {
    return this.gateway.charge(amount, "BRL", token);
  }
}

// Injeção de dependência — fácil de trocar o gateway
const stripe = new StripeAdapter(new LegacyStripeClient());
const service = new PaymentService(stripe);

Decorator

O Decorator adiciona responsabilidades a um objeto dinamicamente, sem alterar sua classe. Favorece composição sobre herança — você empilha comportamentos em vez de criar hierarquias profundas. É o princípio por trás de middlewares do Express e decorators do Python/TypeScript.

// Interface base
interface HttpClient {
  request(url: string, options?: RequestInit): Promise<Response>;
}

// Implementação base
class FetchClient implements HttpClient {
  async request(url: string, options?: RequestInit): Promise<Response> {
    return fetch(url, options);
  }
}

// Decorator: adiciona logging
class LoggingDecorator implements HttpClient {
  constructor(private readonly inner: HttpClient) {}

  async request(url: string, options?: RequestInit): Promise<Response> {
    const start = performance.now();
    console.log(`[HTTP] → ${options?.method ?? "GET"} ${url}`);

    const response = await this.inner.request(url, options);

    const duration = (performance.now() - start).toFixed(2);
    console.log(`[HTTP] ← ${response.status} (${duration}ms)`);

    return response;
  }
}

// Decorator: adiciona retry automático
class RetryDecorator implements HttpClient {
  constructor(
    private readonly inner: HttpClient,
    private readonly maxRetries: number = 3,
    private readonly baseDelay: number = 1000
  ) {}

  async request(url: string, options?: RequestInit): Promise<Response> {
    let lastError: Error | null = null;

    for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
      try {
        const response = await this.inner.request(url, options);
        if (response.ok || response.status < 500) return response;
        throw new Error(`HTTP ${response.status}`);
      } catch (error) {
        lastError = error as Error;
        if (attempt < this.maxRetries) {
          const delay = this.baseDelay * Math.pow(2, attempt); // Backoff exponencial
          await new Promise((resolve) => setTimeout(resolve, delay));
        }
      }
    }

    throw lastError;
  }
}

// Decorator: adiciona autenticação
class AuthDecorator implements HttpClient {
  constructor(
    private readonly inner: HttpClient,
    private readonly getToken: () => string
  ) {}

  async request(url: string, options: RequestInit = {}): Promise<Response> {
    const headers = new Headers(options.headers);
    headers.set("Authorization", `Bearer ${this.getToken()}`);
    return this.inner.request(url, { ...options, headers });
  }
}

// Composição: empilha decorators como camadas
// A ordem importa! Auth → Retry → Logging → Fetch
const client: HttpClient = new AuthDecorator(
  new RetryDecorator(
    new LoggingDecorator(
      new FetchClient()
    ),
    3
  ),
  () => "meu-jwt-token"
);

// O consumidor não sabe quantas camadas existem — vê apenas HttpClient
await client.request("https://api.exemplo.com/users");

Proxy

O Proxy fornece um substituto ou placeholder para outro objeto, controlando o acesso a ele. Usos comuns: lazy loading, cache, logging, validação de acesso. JavaScript tem suporte nativo a proxies via Proxy.

// Proxy nativo do JavaScript — intercepta operações no objeto
function createValidatedObject<T extends Record<string, unknown>>(
  target: T,
  schema: Record<string, (value: unknown) => boolean>
): T {
  return new Proxy(target, {
    set(obj, prop, value) {
      const key = prop as string;
      const validator = schema[key];

      if (validator && !validator(value)) {
        throw new TypeError(
          `Valor inválido para "${key}": ${JSON.stringify(value)}`
        );
      }

      return Reflect.set(obj, prop, value);
    },

    get(obj, prop) {
      const key = prop as string;
      if (!(key in obj)) {
        console.warn(`Acesso a propriedade inexistente: "${key}"`);
        return undefined;
      }
      return Reflect.get(obj, prop);
    },
  });
}

const user = createValidatedObject(
  { name: "Lucas", age: 28, email: "lucas@exemplo.com" },
  {
    age: (v) => typeof v === "number" && (v as number) > 0 && (v as number) < 150,
    email: (v) => typeof v === "string" && (v as string).includes("@"),
  }
);

user.name = "Maria";         // OK
user.age = 30;               // OK
// user.age = -5;            // TypeError: Valor inválido para "age": -5
// user.email = "inválido";  // TypeError: Valor inválido para "email": "inválido"

Proxy para lazy loading — adia a criação de um objeto pesado até o primeiro acesso:

function lazyInit<T extends object>(factory: () => T): T {
  let instance: T | null = null;

  return new Proxy({} as T, {
    get(_, prop, receiver) {
      if (!instance) {
        instance = factory(); // Só cria quando realmente precisa
      }
      return Reflect.get(instance, prop, receiver);
    },
  });
}

// O objeto pesado só é instanciado no primeiro acesso a qualquer propriedade
const heavyService = lazyInit(() => {
  console.log("Inicializando serviço pesado...");
  return { process: (data: string) => `Processado: ${data}` };
});

Facade

O Facade fornece uma interface simplificada para um subsistema complexo. Não esconde a complexidade — apenas oferece um atalho para os casos de uso mais comuns. O código cliente pode ainda acessar o subsistema diretamente quando necessário.

// Subsistemas complexos
class UserRepository {
  async create(data: { name: string; email: string }): Promise<{ id: string }> {
    return { id: crypto.randomUUID() };
  }
}

class EmailService {
  async sendWelcome(email: string, name: string): Promise<void> {
    // Integração com SendGrid, SES, etc.
  }
}

class AnalyticsTracker {
  track(event: string, properties: Record<string, unknown>): void {
    // Envia para Mixpanel, Amplitude, etc.
  }
}

class PermissionService {
  async assignDefaultRole(userId: string): Promise<void> {
    // Atribui role "user" no sistema de permissões
  }
}

// Facade: uma única chamada orquestra 4 subsistemas
class UserOnboardingFacade {
  constructor(
    private readonly users: UserRepository,
    private readonly emails: EmailService,
    private readonly analytics: AnalyticsTracker,
    private readonly permissions: PermissionService
  ) {}

  async registerUser(name: string, email: string): Promise<{ id: string }> {
    const user = await this.users.create({ name, email });

    // Dispara tudo em paralelo — não precisa ser sequencial
    await Promise.all([
      this.emails.sendWelcome(email, name),
      this.permissions.assignDefaultRole(user.id),
    ]);

    this.analytics.track("user_registered", { userId: user.id, name });

    return user;
  }
}

// O controller só conhece a Facade — não sabe dos subsistemas
const onboarding = new UserOnboardingFacade(
  new UserRepository(),
  new EmailService(),
  new AnalyticsTracker(),
  new PermissionService()
);

await onboarding.registerUser("Maria", "maria@exemplo.com");

Behavioral Patterns

Observer (Pub/Sub)

O Observer define uma dependência um-para-muitos: quando o estado de um objeto muda, todos os dependentes são notificados automaticamente. É a base do EventEmitter do Node.js, do modelo reativo do RxJS e dos sistemas de pub/sub.

// Implementação type-safe com generics
type EventMap = Record<string, unknown>;
type EventHandler<T> = (payload: T) => void | Promise<void>;

class TypedEventEmitter<Events extends EventMap> {
  private handlers = new Map<keyof Events, Set<EventHandler<any>>>();

  on<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): () => void {
    if (!this.handlers.has(event)) {
      this.handlers.set(event, new Set());
    }
    this.handlers.get(event)!.add(handler);

    // Retorna função de cleanup — evita memory leaks
    return () => {
      this.handlers.get(event)?.delete(handler);
    };
  }

  async emit<K extends keyof Events>(event: K, payload: Events[K]): Promise<void> {
    const eventHandlers = this.handlers.get(event);
    if (!eventHandlers) return;

    // Executa todos os handlers em paralelo
    const promises = [...eventHandlers].map((handler) => handler(payload));
    await Promise.all(promises);
  }
}

// Definição dos eventos da aplicação — tipagem forte
interface AppEvents {
  "user:created": { id: string; name: string; email: string };
  "user:deleted": { id: string; reason: string };
  "order:placed": { orderId: string; userId: string; total: number };
}

const bus = new TypedEventEmitter<AppEvents>();

// Cada módulo se inscreve nos eventos que lhe interessam
const unsubscribe = bus.on("user:created", async (user) => {
  // TypeScript sabe que `user` é { id: string; name: string; email: string }
  console.log(`Enviando e-mail de boas-vindas para ${user.email}`);
});

bus.on("user:created", (user) => {
  console.log(`[Analytics] Novo usuário: ${user.name}`);
});

bus.on("order:placed", (order) => {
  console.log(`[Faturamento] Pedido ${order.orderId}: R$ ${order.total}`);
});

// Emissão — todos os listeners são notificados
await bus.emit("user:created", { id: "1", name: "Lucas", email: "lucas@x.com" });

// Cleanup para evitar memory leak
unsubscribe();

Strategy

O Strategy permite trocar algoritmos em runtime sem alterar o código que os utiliza. Elimina cadeias de if/else e switch — cada algoritmo é encapsulado em sua própria classe/função. É a materialização do Open/Closed Principle (SOLID).

// Cada estratégia implementa a mesma interface
interface CompressionStrategy {
  compress(data: Buffer): Promise<Buffer>;
  decompress(data: Buffer): Promise<Buffer>;
  readonly name: string;
}

class GzipStrategy implements CompressionStrategy {
  readonly name = "gzip";

  async compress(data: Buffer): Promise<Buffer> {
    // Implementação real usaria zlib.gzip
    return data; // Simplificado
  }

  async decompress(data: Buffer): Promise<Buffer> {
    return data;
  }
}

class BrotliStrategy implements CompressionStrategy {
  readonly name = "brotli";

  async compress(data: Buffer): Promise<Buffer> {
    return data;
  }

  async decompress(data: Buffer): Promise<Buffer> {
    return data;
  }
}

class NoCompressionStrategy implements CompressionStrategy {
  readonly name = "none";
  async compress(data: Buffer): Promise<Buffer> { return data; }
  async decompress(data: Buffer): Promise<Buffer> { return data; }
}

// O contexto usa a estratégia sem saber qual é
class FileProcessor {
  constructor(private strategy: CompressionStrategy) {}

  // Permite trocar em runtime
  setStrategy(strategy: CompressionStrategy): void {
    this.strategy = strategy;
  }

  async processFile(data: Buffer): Promise<Buffer> {
    console.log(`Comprimindo com: ${this.strategy.name}`);
    return this.strategy.compress(data);
  }
}

// Seleção dinâmica baseada em condição
function selectStrategy(fileSizeBytes: number): CompressionStrategy {
  if (fileSizeBytes > 10_000_000) return new BrotliStrategy();  // > 10MB: brotli (melhor ratio)
  if (fileSizeBytes > 100_000) return new GzipStrategy();       // > 100KB: gzip (rápido)
  return new NoCompressionStrategy();                           // Pequeno: sem compressão
}

const processor = new FileProcessor(selectStrategy(5_000_000));

Command

O Command encapsula uma ação como um objeto, permitindo parametrizar, enfileirar, serializar e desfazer operações. É a base de sistemas de undo/redo, filas de tarefas e do padrão CQRS (Command Query Responsibility Segregation).

// Interface do comando — toda ação implementa execute e undo
interface Command {
  execute(): void;
  undo(): void;
  readonly description: string;
}

// Estado compartilhado (documento de texto simplificado)
class TextDocument {
  private content: string[] = [];

  insert(position: number, text: string): void {
    this.content.splice(position, 0, text);
  }

  remove(position: number): string {
    return this.content.splice(position, 1)[0];
  }

  toString(): string {
    return this.content.join("");
  }
}

// Comando concreto: inserir caractere
class InsertCharCommand implements Command {
  constructor(
    private readonly doc: TextDocument,
    private readonly position: number,
    private readonly char: string
  ) {}

  get description() { return `Inserir '${this.char}' na posição ${this.position}`; }

  execute(): void {
    this.doc.insert(this.position, this.char);
  }

  undo(): void {
    this.doc.remove(this.position);
  }
}

// CommandManager: gerencia histórico e undo/redo
class CommandManager {
  private history: Command[] = [];
  private undone: Command[] = [];

  execute(command: Command): void {
    command.execute();
    this.history.push(command);
    this.undone = []; // Novo comando invalida o redo stack
  }

  undo(): void {
    const command = this.history.pop();
    if (!command) return;
    command.undo();
    this.undone.push(command);
  }

  redo(): void {
    const command = this.undone.pop();
    if (!command) return;
    command.execute();
    this.history.push(command);
  }
}

const doc = new TextDocument();
const manager = new CommandManager();

// Cada ação do usuário é um Command
manager.execute(new InsertCharCommand(doc, 0, "H"));
manager.execute(new InsertCharCommand(doc, 1, "i"));
console.log(doc.toString()); // "Hi"

manager.undo();
console.log(doc.toString()); // "H"

manager.redo();
console.log(doc.toString()); // "Hi"

Iterator

O Iterator fornece uma maneira de acessar os elementos de uma coleção sequencialmente sem expor a estrutura interna. Em JavaScript, o protocolo de iteração (Symbol.iterator) e os generators (function*) são a implementação nativa desse pattern.

// Implementando o protocolo de iteração — Symbol.iterator
class NumberRange implements Iterable<number> {
  constructor(
    private readonly start: number,
    private readonly end: number,
    private readonly step: number = 1
  ) {}

  // O objeto é iterável — funciona com for...of, spread, destructuring
  [Symbol.iterator](): Iterator<number> {
    let current = this.start;
    const end = this.end;
    const step = this.step;

    return {
      next(): IteratorResult<number> {
        if (current <= end) {
          const value = current;
          current += step;
          return { value, done: false };
        }
        return { value: undefined, done: true };
      },
    };
  }
}

const range = new NumberRange(1, 10, 2);
for (const n of range) {
  console.log(n); // 1, 3, 5, 7, 9
}

console.log([...range]); // [1, 3, 5, 7, 9] — spread funciona

// Generators: syntax sugar para iteradores complexos
// Lazy evaluation — gera valores sob demanda, sem alocar array
function* paginate<T>(
  items: T[],
  pageSize: number
): Generator<T[], void, unknown> {
  for (let i = 0; i < items.length; i += pageSize) {
    yield items.slice(i, i + pageSize);
  }
}

const allUsers = Array.from({ length: 100 }, (_, i) => `user_${i}`);

for (const page of paginate(allUsers, 10)) {
  console.log(`Página com ${page.length} usuários`);
  // Processa 10 usuários por vez — não carrega tudo em memória
}

// Generator infinito — útil para IDs, sequências, retry delays
function* fibonacciSequence(): Generator<number, never, unknown> {
  let a = 0, b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

const fib = fibonacciSequence();
console.log(fib.next().value); // 0
console.log(fib.next().value); // 1
console.log(fib.next().value); // 1
console.log(fib.next().value); // 2
console.log(fib.next().value); // 3

Chain of Responsibility

A Chain of Responsibility passa uma requisição por uma cadeia de handlers, onde cada handler decide se processa a requisição ou a repassa para o próximo. É exatamente como middlewares funcionam no Express, Koa e em pipelines de processamento.

// Tipagem do middleware — genérica e reutilizável
type Next = () => Promise<void>;

interface Context {
  request: { path: string; method: string; headers: Record<string, string>; body?: unknown };
  response: { status: number; body: unknown; headers: Record<string, string> };
  state: Record<string, unknown>;
}

type Middleware = (ctx: Context, next: Next) => Promise<void>;

// Mini-framework que compõe middlewares — mesmo princípio do Koa
class Pipeline {
  private middlewares: Middleware[] = [];

  use(middleware: Middleware): this {
    this.middlewares.push(middleware);
    return this;
  }

  async execute(ctx: Context): Promise<void> {
    let index = 0;

    const dispatch = async (): Promise<void> => {
      if (index >= this.middlewares.length) return;
      const middleware = this.middlewares[index++];
      await middleware(ctx, dispatch);
    };

    await dispatch();
  }
}

// Cada middleware é um elo da cadeia
const timing: Middleware = async (ctx, next) => {
  const start = Date.now();
  await next(); // Passa para o próximo handler
  const ms = Date.now() - start;
  ctx.response.headers["X-Response-Time"] = `${ms}ms`;
};

const auth: Middleware = async (ctx, next) => {
  const token = ctx.request.headers["authorization"];
  if (!token) {
    ctx.response.status = 401;
    ctx.response.body = { error: "Token ausente" };
    return; // NÃO chama next() — interrompe a cadeia
  }
  ctx.state.userId = "user-from-token";
  await next();
};

const handler: Middleware = async (ctx, _next) => {
  ctx.response.status = 200;
  ctx.response.body = { message: "OK", userId: ctx.state.userId };
};

// Composição da pipeline
const app = new Pipeline();
app.use(timing).use(auth).use(handler);

Patterns Modernos em JavaScript/TypeScript

Module Pattern e Revealing Module

O sistema de módulos do JavaScript (ESM) é, por definição, um Module Pattern. Cada arquivo é um módulo com escopo isolado — só o que é exportado é público. O Revealing Module Pattern expõe explicitamente apenas a API pública:

// revealing-module.ts — Só exporta o que é público
const SECRET_KEY = "internal-only"; // Privado ao módulo
const cache = new Map<string, unknown>(); // Estado encapsulado

function encrypt(data: string): string {
  // Usa SECRET_KEY internamente — nunca exposta
  return Buffer.from(`${SECRET_KEY}:${data}`).toString("base64");
}

function decrypt(encoded: string): string {
  return Buffer.from(encoded, "base64").toString("utf-8").replace(`${SECRET_KEY}:`, "");
}

// API pública — o módulo "revela" apenas o necessário
export { encrypt, decrypt };

Composition over Inheritance

Herança cria acoplamento forte e hierarquias frágeis. Composição monta comportamento a partir de peças independentes. Em TypeScript, você compõe comportamentos com mixins ou funções de ordem superior:

// Em vez de: class AdminUser extends PremiumUser extends User (herança frágil)
// Composição: cada capacidade é independente

type User = { id: string; name: string; email: string };

type WithAuth = { permissions: string[]; hasPermission: (p: string) => boolean };
type WithAudit = { lastLogin: Date; loginCount: number; recordLogin: () => void };

function withAuth(permissions: string[]): WithAuth {
  return {
    permissions,
    hasPermission(p: string) {
      return this.permissions.includes(p);
    },
  };
}

function withAudit(): WithAudit {
  return {
    lastLogin: new Date(),
    loginCount: 0,
    recordLogin() {
      this.lastLogin = new Date();
      this.loginCount++;
    },
  };
}

// Composição: monta o objeto com as capacidades necessárias
function createAdminUser(name: string, email: string): User & WithAuth & WithAudit {
  return {
    id: crypto.randomUUID(),
    name,
    email,
    ...withAuth(["read", "write", "delete", "admin"]),
    ...withAudit(),
  };
}

const admin = createAdminUser("Maria", "maria@exemplo.com");
admin.recordLogin();
console.log(admin.hasPermission("admin")); // true

Anti-Patterns

Anti-patterns são soluções que parecem boas mas geram problemas sérios a longo prazo.

God Object       Um objeto que sabe/faz tudo. Classe com 3000 linhas que gerencia
                 usuários, pagamentos, e-mails e logs. Violação brutal do SRP.
                 → Solução: Separar em classes com responsabilidades únicas.

Spaghetti Code   Fluxo de controle emaranhado — impossível de seguir. Callbacks
                 aninhados (callback hell), goto statements, acoplamento implícito.
                 → Solução: Extrair funções, usar async/await, aplicar patterns.

Lava Flow        Código morto ou experimental que ninguém tem coragem de remover
                 porque "pode quebrar algo". Acumula-se como lava solidificada.
                 → Solução: Testes que garantem que a remoção é segura. Git existe.

Golden Hammer    "Eu sei usar Redis, então tudo é problema de cache." Usar a
                 mesma ferramenta/pattern para todo problema, ignorando contexto.
                 → Solução: Avaliar trade-offs objetivamente. Escolher a ferramenta certa.

Premature        Abstrair antes de entender o problema. Criar factories, strategies
Abstraction      e observers para um CRUD de 3 endpoints que nunca vai mudar.
                 → Solução: Regra dos três — abstraia quando o padrão se repetir 3x.

Quando NÃO Usar Patterns

A pior coisa que um engenheiro pode fazer é aplicar um pattern só porque aprendeu ele ontem.

  • YAGNI (You Aren’t Gonna Need It): Não crie abstrações para requisitos que não existem. Um Singleton para um serviço que nunca precisará de instância única é complexidade gratuita.
  • KISS (Keep It Simple, Stupid): Se uma função de 10 linhas resolve o problema, não crie 4 classes com interfaces e factories.
  • Regra dos Três: Não abstraia na primeira duplicação. Espere o padrão se repetir pelo menos três vezes antes de extrair um pattern.
  • Custo de indireção: Cada pattern adiciona uma camada de abstração. Abstrações têm custo cognitivo — o próximo desenvolvedor precisa entender a indireção para debugar.
// OVER-ENGINEERING: Factory + Strategy + Observer para enviar um e-mail
// ❌ Não faça isso para um caso simples

// ✅ Faça isso:
async function sendWelcomeEmail(to: string, name: string): Promise<void> {
  await emailClient.send({
    to,
    subject: "Bem-vindo!",
    body: `Olá, ${name}!`,
  });
}

// Patterns devem EMERGIR da necessidade — não serem impostos de antemão.
// Quando o sendWelcomeEmail precisar de retry, logging e múltiplos providers,
// AÍ você refatora para Decorator, Strategy, etc.

Resumo prático: Patterns são ferramentas, não regras. Domine-os para reconhecer quando um problema se encaixa em uma solução já catalogada — mas nunca force um pattern onde simplicidade resolve.


Referências e Fontes

  • “Design Patterns: Elements of Reusable Object-Oriented Software” — Gamma, Helm, Johnson, Vlissides (GoF, 1994) — o catálogo original
  • “Head First Design Patterns” — Freeman & Robson — versão mais acessível e prática
  • “Refactoring: Improving the Design of Existing Code” — Martin Fowler — quando e como aplicar patterns em código existente
  • “Patterns of Enterprise Application Architecture” — Martin Fowler — patterns para aplicações corporativas
  • Refactoring.Guruhttps://refactoring.guru/design-patterns — catálogo visual com exemplos em múltiplas linguagens
  • Source Makinghttps://sourcemaking.com/design_patterns — referência online com anti-patterns