Padrões Comportamentais
Baixar PDFObserver, Strategy, Command e Middleware em TypeScript — como objetos colaboram e comunicam entre si.
Padrões Comportamentais em TypeScript
Padrões comportamentais respondem à pergunta: "Como objetos se comunicam e colaboram?". Eles definem os padrões de interação entre objetos.
Observer Pattern
O que é
Define uma dependência um-para-muitos entre objetos: quando um objeto muda de estado, todos os dependentes são notificados automaticamente.
Analogia
Uma newsletter: quando sai uma nova edição, todos os assinantes recebem. O editor não precisa enviar individualmente — ele publica uma vez e o sistema distribui.
Onde você já usa sem saber
- React
useState— quando o state muda, todos os componentes que o consomem re-renderizam - DOM Events —
addEventListeneré Observer puro - RxJS — Observables são Observer pattern esteroides
- EventEmitter do Node.js —
server.on('request', handler)
Backend — Event emitter tipado
// Event map — define quais eventos existem e seus payloads
interface AppEvents {
'user:registered': { userId: string; email: string };
'order:placed': { orderId: string; userId: string; total: number };
'order:shipped': { orderId: string; trackingCode: string };
'payment:received': { paymentId: string; orderId: string; amount: number };
}
// Emitter tipado — TS garante que os eventos e payloads estão corretos
class TypedEmitter<TEvents extends Record<string, unknown>> {
private listeners = new Map<keyof TEvents, Set<(data: any) => void>>();
on<K extends keyof TEvents>(event: K, handler: (data: TEvents[K]) => void): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(handler);
}
off<K extends keyof TEvents>(event: K, handler: (data: TEvents[K]) => void): void {
this.listeners.get(event)?.delete(handler);
}
emit<K extends keyof TEvents>(event: K, data: TEvents[K]): void {
this.listeners.get(event)?.forEach(handler => handler(data));
}
}
// Instância global de eventos
const appEvents = new TypedEmitter<AppEvents>();
// Service que EMITE eventos — não sabe quem escuta
class UserService {
async register(dto: CreateUserDto): Promise<User> {
const user = await this.repo.create(dto);
appEvents.emit('user:registered', { userId: user.id, email: user.email }); // ✅ dispara e esquece
return user;
}
}
// Listeners — cada um faz sua coisa
// Email
appEvents.on('user:registered', async ({ email, userId }) => {
await emailService.sendWelcome(email);
logger.info(`Email de boas-vindas enviado para ${userId}`);
});
// Analytics
appEvents.on('user:registered', async ({ userId }) => {
await analytics.track('user_signup', { userId });
});
// Audit log
appEvents.on('user:registered', async ({ userId }) => {
await auditLog.record('USER_REGISTERED', userId);
});
// ✅ Adicionar um novo listener NÃO altera UserService
// Slack notification? Adicione mais um appEvents.on(...)Frontend — Custom event bus
// Event bus para comunicação entre componentes sem prop drilling
type UIEvents = {
'cart:updated': { itemCount: number; total: number };
'modal:open': { type: 'login' | 'signup' | 'checkout' };
'modal:close': {};
'theme:change': { mode: 'light' | 'dark' };
};
const uiEvents = new TypedEmitter<UIEvents>();
// Componente que MODIFICA o carrinho
function AddToCartButton({ product }: { product: Product }) {
const handleClick = () => {
addToCart(product);
uiEvents.emit('cart:updated', { itemCount: getCartCount(), total: getCartTotal() });
};
return <button onClick={handleClick}>Adicionar ao carrinho</button>;
}
// Componente que ESCUTA — em outro lugar da árvore
function CartBadge() {
const [count, setCount] = useState(0);
useEffect(() => {
const handler = ({ itemCount }: { itemCount: number }) => setCount(itemCount);
uiEvents.on('cart:updated', handler);
return () => uiEvents.off('cart:updated', handler); // cleanup
}, []);
return count > 0 ? <span className="badge">{count}</span> : null;
}Strategy Pattern
O que é
Define uma família de algoritmos, encapsula cada um e os torna intercambiáveis. O cliente escolhe qual algoritmo usar em runtime.
Analogia
Meios de transporte: para ir ao trabalho, você pode ir de carro, bicicleta ou metrô. O destino é o mesmo — o "como" muda conforme contexto (distância, clima, orçamento).
Frontend — Estratégia de validação
// Interface da estratégia
interface ValidationStrategy<T> {
validate(value: T): Result<T, string>;
}
// Estratégias concretas
const emailStrategy: ValidationStrategy<string> = {
validate(value) {
if (!value.includes('@')) return { ok: false, error: 'Email inválido' };
return { ok: true, value };
},
};
const passwordStrategy: ValidationStrategy<string> = {
validate(value) {
if (value.length < 8) return { ok: false, error: 'Senha deve ter pelo menos 8 caracteres' };
if (!/[A-Z]/.test(value)) return { ok: false, error: 'Senha deve ter uma letra maiúscula' };
if (!/[0-9]/.test(value)) return { ok: false, error: 'Senha deve ter um número' };
return { ok: true, value };
},
};
const cpfStrategy: ValidationStrategy<string> = {
validate(value) {
const digits = value.replace(/\D/g, '');
if (digits.length !== 11) return { ok: false, error: 'CPF deve ter 11 dígitos' };
return { ok: true, value };
},
};
// Validador genérico — aceita qualquer estratégia
function validateField<T>(value: T, strategy: ValidationStrategy<T>): Result<T, string> {
return strategy.validate(value);
}
// Uso — troca a estratégia conforme o campo
validateField('user@email', emailStrategy);
validateField('MinhaSenh4', passwordStrategy);
validateField('123.456.789-00', cpfStrategy);Backend — Estratégia de ordenação
// Interface
interface SortStrategy<T> {
sort(items: T[]): T[];
}
// Estratégias
class SortByName<T extends { name: string }> implements SortStrategy<T> {
sort(items: T[]): T[] {
return [...items].sort((a, b) => a.name.localeCompare(b.name));
}
}
class SortByPrice<T extends { price: number }> implements SortStrategy<T> {
sort(items: T[]): T[] {
return [...items].sort((a, b) => a.price - b.price);
}
}
class SortByDate<T extends { createdAt: Date }> implements SortStrategy<T> {
sort(items: T[]): T[] {
return [...items].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
}
}
// Service que usa a estratégia
class ProductService {
async getProducts(sortBy: 'name' | 'price' | 'date'): Promise<Product[]> {
const products = await this.repo.findAll();
const strategies: Record<string, SortStrategy<Product>> = {
name: new SortByName(),
price: new SortByPrice(),
date: new SortByDate(),
};
return strategies[sortBy].sort(products);
}
}Command Pattern
O que é
Encapsula uma requisição como um objeto, permitindo parametrizar clientes com diferentes requisições, enfileirar operações e suportar undo/redo.
Analogia
Um controle remoto universal: cada botão é um "command". O botão não sabe o que o aparelho faz — ele apenas executa o command associado. Você pode reprogramar os botões sem trocar o controle.
Frontend — Undo/Redo para editor
// Interface do command
interface Command {
execute(): void;
undo(): void;
description: string;
}
// Commands concretos
class InsertTextCommand implements Command {
private previousText: string = '';
constructor(
private editor: TextEditor,
private text: string,
private position: number,
) {}
description = `Inserir "${this.text}" na posição ${this.position}`;
execute() {
this.previousText = this.editor.content;
this.editor.content =
this.editor.content.slice(0, this.position) +
this.text +
this.editor.content.slice(this.position);
}
undo() {
this.editor.content = this.previousText;
}
}
class DeleteTextCommand implements Command {
private deletedText: string = '';
constructor(
private editor: TextEditor,
private start: number,
private end: number,
) {}
description = `Deletar de ${this.start} a ${this.end}`;
execute() {
this.deletedText = this.editor.content.slice(this.start, this.end);
this.editor.content =
this.editor.content.slice(0, this.start) + this.editor.content.slice(this.end);
}
undo() {
this.editor.content =
this.editor.content.slice(0, this.start) +
this.deletedText +
this.editor.content.slice(this.start);
}
}
// Gerenciador de commands — mantém histórico
class CommandHistory {
private history: Command[] = [];
private redoStack: Command[] = [];
execute(command: Command): void {
command.execute();
this.history.push(command);
this.redoStack = []; // limpa redo ao executar novo command
}
undo(): Command | null {
const command = this.history.pop();
if (!command) return null;
command.undo();
this.redoStack.push(command);
return command;
}
redo(): Command | null {
const command = this.redoStack.pop();
if (!command) return null;
command.execute();
this.history.push(command);
return command;
}
}
// Hook React
function useEditor() {
const [editor] = useState(() => new TextEditor());
const [history] = useState(() => new CommandHistory());
const [, forceUpdate] = useState(0);
const insert = (text: string, position: number) => {
history.execute(new InsertTextCommand(editor, text, position));
forceUpdate(n => n + 1);
};
const undo = () => { history.undo(); forceUpdate(n => n + 1); };
const redo = () => { history.redo(); forceUpdate(n => n + 1); };
return { content: editor.content, insert, undo, redo };
}Backend — Command para fila de jobs
// Interface
interface JobCommand {
readonly name: string;
execute(): Promise<void>;
rollback?(): Promise<void>;
}
// Commands
class SendEmailJob implements JobCommand {
readonly name = 'send-email';
constructor(private to: string, private subject: string, private body: string) {}
async execute() {
await emailService.send({ to: this.to, subject: this.subject, body: this.body });
}
}
class GenerateReportJob implements JobCommand {
readonly name = 'generate-report';
constructor(private reportId: string, private filters: ReportFilters) {}
async execute() {
const data = await dataService.query(this.filters);
const pdf = await pdfGenerator.generate(data);
await storage.save(`reports/${this.reportId}.pdf`, pdf);
}
}
// Queue — executa commands em sequência ou paralelo
class JobQueue {
private queue: JobCommand[] = [];
enqueue(job: JobCommand): void {
this.queue.push(job);
}
async processAll(): Promise<void> {
for (const job of this.queue) {
try {
await job.execute();
logger.info(`Job ${job.name} concluído`);
} catch (err) {
logger.error(`Job ${job.name} falhou`, err as Error);
if (job.rollback) await job.rollback();
}
}
this.queue = [];
}
}
// Uso
const queue = new JobQueue();
queue.enqueue(new SendEmailJob('[email protected]', 'Pedido confirmado', 'Seu pedido #123...'));
queue.enqueue(new GenerateReportJob('report-123', { dateRange: 'last-month' }));
await queue.processAll();Middleware Pattern
O que é
Uma cadeia de processadores sequenciais onde cada um pode processar a requisição e passar para o próximo. É o padrão que o Express, Koa e NestJS usam nativamente.
Backend — Middleware tipado
// Interface
interface Context {
req: Request;
res: Response;
user?: User;
metadata: Record<string, unknown>;
}
type Middleware = (ctx: Context, next: () => Promise<void>) => Promise<void>;
// Middleware chain
class MiddlewareChain {
private middlewares: Middleware[] = [];
use(middleware: Middleware): this {
this.middlewares.push(middleware);
return this;
}
async execute(ctx: Context): Promise<void> {
let index = 0;
const next = async (): Promise<void> => {
if (index < this.middlewares.length) {
const middleware = this.middlewares[index++];
await middleware(ctx, next);
}
};
await next();
}
}
// Middlewares
const authMiddleware: Middleware = async (ctx, next) => {
const token = ctx.req.headers.authorization?.split(' ')[1];
if (!token) {
ctx.res.status(401).json({ error: 'Token não fornecido' });
return; // não chama next — interrompe a cadeia
}
ctx.user = await verifyToken(token);
await next(); // passa para o próximo
};
const rateLimitMiddleware: Middleware = async (ctx, next) => {
const ip = ctx.req.ip;
const hits = await rateLimiter.check(ip);
if (hits > 100) {
ctx.res.status(429).json({ error: 'Muitas requisições' });
return;
}
await next();
};
const loggingMiddleware: Middleware = async (ctx, next) => {
const start = Date.now();
await next();
const duration = Date.now() - start;
logger.info(`${ctx.req.method} ${ctx.req.path} — ${duration}ms`);
};
// Uso
const chain = new MiddlewareChain();
chain.use(loggingMiddleware);
chain.use(rateLimitMiddleware);
chain.use(authMiddleware);