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.Guru — https://refactoring.guru/design-patterns — catálogo visual com exemplos em múltiplas linguagens
- Source Making — https://sourcemaking.com/design_patterns — referência online com anti-patterns