A tomada da parede
Comprar um abajur não exige chamar um eletricista para abrir a parede: você pluga na tomada. A instalação elétrica foi fechada para modificação e aberta para extensão no dia em que alguém padronizou o formato do plugue. Note que o padrão foi definido antes, e não para cada aparelho novo — é isso que o torna útil.
O problema
Certas funções viram ímãs de mudança: toda semana alguém acrescenta mais um caso ao mesmo switch. Elas crescem, ficam difíceis de ler e concentram o risco de regressão.
O incômodo real não é estético. Cada modificação em código testado exige re-testar, revisar e arriscar — mesmo quando o requisito novo nada tem a ver com os antigos.
O ponto sutil é que o princípio não proíbe modificar código. Ele diz que, para o eixo que você já sabe que varia, deveria existir um ponto de extensão. Correções de bug, mudanças de requisito no núcleo e refatorações continuam sendo modificação legítima.
Isso leva ao equilíbrio mais importante da prática: OCP aplicado cedo demais é adivinhação. Abstrair um eixo antes de saber que ele varia costuma produzir a abstração errada — e uma abstração errada custa mais do que o if que ela substituiu, porque agora é preciso desfazê-la.
A heurística que funciona é esperar a evidência. Na primeira ocorrência, escreva direto. Na segunda, observe. Na terceira, o eixo já se declarou e a abstração pode ser desenhada sobre fatos, não sobre suposição.
Como se fecha um módulo
OCP é um objetivo, não uma técnica. Vários mecanismos o alcançam, e a escolha depende de quanto de variação existe e de quem precisa estender.
Strategy é o mais direto: o comportamento variável vira um objeto injetado. É a forma usada no exemplo e serve à maioria dos casos de regra de negócio.
Template Method fecha a sequência e abre os passos — útil quando a ordem é a invariante e o miolo é o que varia.
Registro de handlers (um mapa de tipo → implementação) funciona bem quando as variantes são muitas e descobertas em tempo de execução, como em plugins.
E há a opção de não usar padrão nenhum: em linguagens funcionais, passar uma função como parâmetro alcança o mesmo fechamento com muito menos cerimônia. O princípio é sobre onde a mudança chega, não sobre quantas classes existem.
Aberto para extensão, fechado para modificação
- promoção nova é uma classe nova
- o carrinho não muda mais por causa de promoções
- cada desconto é testável isoladamente
- quem estende não precisa entender o resto
O custo é ter acertado o eixo: se amanhã a variação for por região em vez de por tipo de promoção, a abstração escolhida atrapalha em vez de ajudar.
Carrinho (fechado)depende só da interface Desconto
interface Descontoo ponto de extensão
Implementações (abertas)crescem sem tocar no carrinho
DescontoNatalexistente
DescontoFidelidadeexistente
DescontoCupomadicionado hoje
Navegue pelas camadas
Camada 1/3O eixo de variação
use ← →Camada 1
O eixo de variação
o que realmente muda
A dimensão em que os requisitos novos chegam: tipo de desconto, formato de saída, meio de pagamento. Descobri-lo é o trabalho difícil; abstraí-lo é a parte mecânica.
No código
// três meses de commits mostraram: o que varia é a regra de descontoSe violar: escolher o eixo errado cria uma abstração que precisa ser furada a cada requisito — pior do que não ter abstração.
Estrutura
Código
// ANTES: cada promocao nova reabre a mesma funcao
function totalRuim(valor: number, tipo: string): number {
if (tipo === "natal") return valor * 0.9;
if (tipo === "fidelidade") return valor - 10;
if (tipo === "primeira-compra") return valor * 0.85;
return valor; // e assim por diante, para sempre
}
// DEPOIS: o eixo de variacao vira abstracao
interface Desconto {
aplicar(valor: number): number;
}
class DescontoNatal implements Desconto {
aplicar(valor: number): number {
return valor * 0.9;
}
}
class DescontoFidelidade implements Desconto {
aplicar(valor: number): number {
return Math.max(0, valor - 10);
}
}
class Carrinho {
constructor(private descontos: Desconto[] = []) {}
// esta classe nao muda mais quando surge uma promocao nova
total(bruto: number): number {
return this.descontos.reduce((v, d) => d.aplicar(v), bruto);
}
}
// promocao nova = classe nova, zero alteracao no que existe
class DescontoCupom implements Desconto {
constructor(private percentual: number) {}
aplicar(valor: number): number {
return valor * (1 - this.percentual);
}
}
console.log(new Carrinho([new DescontoNatal(), new DescontoCupom(0.05)]).total(200));from abc import ABC, abstractmethod
# ANTES: cada promocao nova reabre a mesma funcao
def total_ruim(valor, tipo):
if tipo == "natal":
return valor * 0.9
if tipo == "fidelidade":
return valor - 10
if tipo == "primeira-compra":
return valor * 0.85
return valor
# DEPOIS: o eixo de variacao vira abstracao
class Desconto(ABC):
@abstractmethod
def aplicar(self, valor): ...
class DescontoNatal(Desconto):
def aplicar(self, valor):
return valor * 0.9
class DescontoFidelidade(Desconto):
def aplicar(self, valor):
return max(0, valor - 10)
class Carrinho:
def __init__(self, descontos=()):
self._descontos = list(descontos)
# esta classe nao muda mais quando surge uma promocao nova
def total(self, bruto):
for d in self._descontos:
bruto = d.aplicar(bruto)
return bruto
# promocao nova = classe nova, zero alteracao no que existe
class DescontoCupom(Desconto):
def __init__(self, percentual):
self._p = percentual
def aplicar(self, valor):
return valor * (1 - self._p)
print(Carrinho([DescontoNatal(), DescontoCupom(0.05)]).total(200))Casos de uso
Meios de pagamento em checkout
Cenário
O checkout aceitava cartão e boleto; em um ano entraram Pix, carteira digital e pagamento parcelado em duas formas diferentes.
Como o padrão entra
Cada meio implementa a mesma interface de cobrança e se registra num catálogo. O fluxo de checkout parou de mudar; meios novos entram sem tocar nele.
Trade-off: Meios de pagamento têm fluxos genuinamente distintos (Pix é assíncrono, cartão tem pré-autorização). Uma interface única acaba com métodos que só alguns implementam de verdade, e a abstração começa a mentir.
Exportadores de relatório
Cenário
Um relatório exportava CSV; depois vieram XLSX, PDF e um formato específico exigido por um cliente grande.
Como o padrão entra
Um exportador por formato, escolhido por um registro. Adicionar o formato do cliente foi uma classe nova, sem risco para os três já em produção.
Trade-off: Formatos com capacidades diferentes (PDF tem paginação, CSV não) forçam a interface ao menor denominador comum, e recursos específicos acabam vazando por opções extras que só um exportador entende.
Plugins de terceiros
Cenário
Uma plataforma precisa permitir que integradores adicionem comportamento sem acesso ao código-fonte nem ao ciclo de release.
Como o padrão entra
O ponto de extensão é publicado como contrato versionado; plugins são carregados em tempo de execução. Aqui o princípio deixa de ser conselho e vira requisito de arquitetura.
Trade-off: A interface vira compromisso público e praticamente imutável: qualquer mudança quebra plugins de terceiros, então erros de desenho ficam para sempre — ou exigem manter duas versões em paralelo.
- 1
Esperar o eixo se declarar
Na primeira ocorrência, escreva direto. Na segunda, observe. Na terceira, o eixo de variação já apareceu com evidência — não com adivinhação.
- 2
Extrair a abstração mínima
Uma interface pequena e estável no eixo certo. Cada método a mais aqui é trabalho para todas as implementações futuras.
- 3
Depender só do contrato
O código que orquestra chama a abstração e para de conhecer concretos. Variante nova = classe nova, zero diff no núcleo.
- 4
Banir o instanceof no fechado
Se o trecho 'fechado' pergunta o tipo concreto, a porta reabriu. A decisão por variante mora na composição, não no orquestrador.
O OCP com instanceof no miolo
Compila, roda, passa no code review. E quebra depois.
Existe interface, existem classes por promoção, o diagrama está bonito — e o carrinho ainda ramifica com instanceof. A abstração é decoração; o eixo continua aberto no lugar errado.
interface Desconto {
aplicar(valor: number): number;
}
class DescontoNatal implements Desconto {
aplicar(valor: number) { return valor * 0.9; }
}
class DescontoFidelidade implements Desconto {
aplicar(valor: number) { return Math.max(0, valor - 10); }
}
class Carrinho {
// A interface existe... e o codigo "fechado" continua aberto.
total(bruto: number, d: Desconto): number {
if (d instanceof DescontoNatal) return bruto * 0.9; // furou
if (d instanceof DescontoFidelidade) return bruto - 10; // furou
return d.aplicar(bruto);
}
}
// Promocao nova = editar Carrinho de novo. O Aberto/Fechado
// virou cerimonia: classes novas + o mesmo if de sempre.O que quebra
- Ao adicionar promoçãoÉ preciso reabrir o
Carrinhoe acrescentar mais um ramo — exatamente o que o princípio prometia evitar. - No testeCobrir o total exige montar cada concreta real; o polimorfismo não está sendo exercitado de verdade.
- Na revisãoO
instanceofse espalha para outros métodos (frete, cashback) e cada um sai de sincronia com os demais.
A saída: Chame só d.aplicar(bruto). Se ainda for preciso escolher a concreta, o mapa mora na composição da aplicação — nunca dentro do código que deveria estar fechado.
Armadilhas
Abstrair antes da evidência
Criar uma interface porque 'um dia pode variar' é a forma mais comum de complexidade acidental. Com um implementador só, a interface é adivinhação — e quando o segundo caso chega, quase nunca encaixa. Espere o terceiro caso concreto antes de fechar o eixo.
Abstração furada
O sintoma é o
if (x instanceof Y)reaparecendo no código que deveria estar fechado, ou um parâmetroopcoesgenérico que só uma implementação entende. Quando isso acontece, o eixo escolhido não era o certo — e insistir na abstração custa mais que voltar atrás.Fechar tudo, por precaução
Aplicar OCP em todos os pontos produz um sistema de interfaces com um implementador cada, onde seguir uma chamada exige abrir cinco arquivos. O princípio serve aos eixos que comprovadamente variam; os demais ficam melhor escritos de forma direta.
Quando usar × quando evitar
Use quando
3- O mesmo trecho é reaberto repetidamente para acomodar variantes.
- O eixo de variação já se mostrou em pelo menos três casos.
- Terceiros precisam estender sem alterar seu código.
Evite quando
3- Você ainda não sabe o que vai variar.
- A variação é única e sem sinal de repetição.
- A indireção custa mais que o ramo condicional.
Testar o que ficou
3 armadilhas de OCP — Aberto/Fechado viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.
Onde isto aparece de verdade
Você já usa isto, talvez sem saber o nome.
sistema de plugins (ESLint, Babel)Você adiciona uma regra ou transformação escrevendo um plugin novo, sem nunca editar o núcleo — aberto para extensão, fechado para modificação.middleware do ExpressCada comportamento novo (auth, log, cors) entra como uma função na cadeia; o roteador em si não muda a cada requisito que aparece.extensões do VS CodeO editor ganha linguagens, temas e comandos novos por extensões instaladas, com o executável do VS Code intacto entre uma e outra.
O padrão em uma linha
Sem nome de domínio, sem cerimônia. Só a ideia.
// Estende sem editar o núcleo fechado.
pagamentos.registrar("pix", novoPixStrategy);O que ele cobra
1 salto a mais entre a chamada e o efeito.
- Um ponto de extensão a projetar e manter antes mesmo de saber se aquilo vai variar
- Abstração escolhida no eixo errado engessa justamente onde a mudança de fato acontece
Não vale se: não há sinal de que aquele comportamento vá variar — abstrair por antecipação é complexidade paga sem retorno.
Onde isto aparece
Continue por aqui
- SRP — Responsabilidade ÚnicaUm módulo deve ter apenas uma razão para mudar — ou seja, deve responder a um único interessado do negócio, e não acumular decisões que evoluem por motivos diferentes.
- LSP — Substituição de LiskovUm subtipo deve poder substituir seu supertipo sem que o código que usa o supertipo perceba a diferença — o que é uma exigência sobre comportamento, não apenas sobre assinatura.
- StrategyEncapsula uma família de algoritmos intercambiáveis por trás de uma interface comum, deixando o cliente trocar a estratégia em tempo de execução.