ThiagoSoares
Voltar para o blog
Arquitetura11 min

OCP - Open/Closed Principle

Como adicionar comportamentos em aplicações Node.js com TypeScript sem acumular condicionais em fluxos estáveis.

Compartilhar artigo

Sistemas mudam. Novos meios de pagamento, formatos de relatório e canais de notificação surgem mesmo quando o fluxo principal já está estável.

O Open/Closed Principle, ou OCP, orienta como receber essas variações: permitir que o comportamento seja estendido sem exigir alterações recorrentes no código que já funciona.

O problema que o princípio resolve

Um exportador começa atendendo CSV. Depois recebe JSON, XML e formatos específicos de parceiros. A solução mais imediata costuma ser uma cadeia de condicionais.

Cada novo formato faz o módulo central crescer. Uma mudança localizada passa a exigir edição no mesmo arquivo, novos imports e repetição de testes de todos os caminhos anteriores.

Aberto e fechado

“Aberto para extensão” significa oferecer um ponto explícito para adicionar comportamento. “Fechado para modificação” significa proteger a política estável de alterações causadas por cada nova variação.

Fechado não significa imutável. Bugs, requisitos centrais e contratos ainda exigem edição. O princípio busca separar o que varia do que permanece estável.

Exemplo ruim

code
type ReportFormat = "csv" | "json" | "txt";
 
type SalesRecord = {
  orderId: string;
  customerName: string;
  totalInCents: number;
};
 
class SalesReportExporter {
  export(records: SalesRecord[], format: ReportFormat): string {
    if (format === "json") {
      return JSON.stringify(records, null, 2);
    }
 
    if (format === "csv") {
      const header = "order_id,customer_name,total_in_cents";
      const rows = records.map((record) =>
        [record.orderId, record.customerName, record.totalInCents].join(",")
      );
 
      return [header, ...rows].join("\n");
    }
 
    return records
      .map((record) =>
        `${record.orderId} | ${record.customerName} | ${record.totalInCents}`
      )
      .join("\n");
  }
}

Por que o exemplo é ruim?

O exportador conhece todas as representações. Adicionar XML exige modificar o tipo, importar recursos e editar o método. Regras como escape de CSV tornam a classe ainda mais distante de sua função de selecionar uma saída.

Os formatos também não podem evoluir de forma independente. Um conflito no módulo central pode acontecer mesmo quando duas pessoas trabalham em implementações diferentes.

Refatoração

Primeiro definimos o ponto de extensão a partir do comportamento necessário:

code
interface ReportExporter {
  readonly format: string;
  export(records: SalesRecord[]): string;
}
 
class JsonReportExporter implements ReportExporter {
  readonly format = "json";
 
  export(records: SalesRecord[]): string {
    return JSON.stringify(records, null, 2);
  }
}
 
class CsvReportExporter implements ReportExporter {
  readonly format = "csv";
 
  export(records: SalesRecord[]): string {
    const escape = (value: string | number) =>
      `"${String(value).replaceAll('"', '""')}"`;
    const header = "order_id,customer_name,total_in_cents";
    const rows = records.map((record) =>
      [record.orderId, record.customerName, record.totalInCents]
        .map(escape)
        .join(",")
    );
 
    return [header, ...rows].join("\n");
  }
}

Exemplo bom

O caso de uso procura uma implementação registrada. Ele não muda quando um novo formato é criado:

code
class ExportSalesReport {
  private readonly exporters: Map<string, ReportExporter>;
 
  constructor(exporters: ReportExporter[]) {
    this.exporters = new Map(
      exporters.map((exporter) => [exporter.format, exporter])
    );
  }
 
  execute(records: SalesRecord[], format: string): string {
    const exporter = this.exporters.get(format);
 
    if (!exporter) {
      throw new Error(`Formato de relatório não suportado: ${format}`);
    }
 
    return exporter.export(records);
  }
}
 
const exportSalesReport = new ExportSalesReport([
  new CsvReportExporter(),
  new JsonReportExporter()
]);

Adicionar outro formato significa implementar ReportExporter e registrá-lo na composição da aplicação. A regra de seleção continua intacta.

Onde deve ficar o ponto de extensão?

Nem toda condicional viola OCP. Condições que expressam uma regra central podem ser mais claras do que polimorfismo. O problema aparece quando a condicional enumera uma família aberta de comportamentos.

O contrato também deve refletir a variação real. Se certos formatos precisam de dados completamente diferentes, forçá-los na mesma interface cria parâmetros opcionais e enfraquece o desenho.

Benefícios

  • novas variações ficam isoladas;
  • o fluxo estável sofre menos regressões;
  • implementações podem ser testadas separadamente;
  • equipes trabalham com menos conflitos;
  • detalhes de cada formato não vazam para o orquestrador.

Quando aplicar?

Sempre pergunte: "Adicionar um novo comportamento exige modificar repetidamente um fluxo que já funciona?" Se a resposta for "sim", é hora de aplicar o OCP.

Fechamento

Open/Closed Principle não elimina mudanças. Ele direciona mudanças novas para pontos de extensão e preserva o núcleo que já foi validado.

Quando a variação é real e frequente, um contrato pequeno reduz condicionais e deixa o sistema crescer por adição. Quando ela não existe, código direto continua sendo a melhor escolha.

Série SOLID

Este artigo faz parte de uma série de artigos: