Kaique Mitsuo Silva Yamamoto
Arquitetura softwareFrontend webTypeScriptDesign Patterns

Padrões Comportamentais

Observer, 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);

Referências

On this page