Pular para o conteúdo

OCP - Open-Closed Principle

O princípio é atribuído a Bertrand Meyer (Object-Oriented Software Construction, 1988) e depois reinterpretado por Robert C. Martin. A formulação clássica: “A module should be open for extension but closed for modification.”

  • Aberto para extensão: o comportamento do módulo pode ser ampliado para atender a novos requisitos.
  • Fechado para modificação: estender esse comportamento não exige alterar o código-fonte existente do módulo.

À primeira vista soa contraditório — como estender sem modificar? A resposta é abstração. Um módulo depende de uma interface estável (fechada); novos comportamentos chegam como novas implementações dessa interface (aberto). O código cliente não precisa ser tocado.

Martin, em Clean Architecture, eleva o OCP a princípio arquitetural: o objetivo é organizar o sistema de forma que mudanças de requisito impactem a menor quantidade possível de código, e que componentes de alto nível fiquem protegidos de mudanças em componentes de baixo nível — o que amarra o OCP ao DIP - Dependency Inversion Principle.

O anti-padrão que o OCP combate é a cadeia de if/else ou switch que cresce a cada novo caso. Cada novo tipo de desconto, cada novo meio de pagamento, cada novo formato de exportação obriga a abrir a mesma classe e adicionar um ramo. Consequências:

  • Alto risco de regressão: você reabre e recompila código já validado.
  • Violação implícita de SRP: a classe passa a conhecer todos os casos.
  • Testes existentes precisam ser reexecutados e frequentemente reescritos.

OCP tem um custo real: abstração antecipada é uma aposta. Criar pontos de extensão exige prever quais eixos de mudança vão ocorrer. Se você abstrai o eixo errado, paga o preço da indireção sem colher o benefício — e ainda dificulta a mudança que realmente aparecer.

Não aplique OCP quando o conjunto de casos é genuinamente fechado e estável (ex.: dias da semana), ou quando o custo da indireção supera o risco de modificação em código raramente alterado.

Cada novo meio de pagamento exige abrir e modificar ProcessadorPagamento.

public enum TipoPagamento { CARTAO, PIX, BOLETO }
public record Pagamento(TipoPagamento tipo, BigDecimal valor, String documento) {}
@Service
public class ProcessadorPagamento {
public String processar(Pagamento pagamento) {
if (pagamento.tipo() == TipoPagamento.CARTAO) {
return "Cobrado " + pagamento.valor() + " no cartão";
} else if (pagamento.tipo() == TipoPagamento.PIX) {
return "Gerado QR Code PIX para " + pagamento.valor();
} else if (pagamento.tipo() == TipoPagamento.BOLETO) {
return "Emitido boleto de " + pagamento.valor();
}
throw new IllegalArgumentException("Tipo não suportado: " + pagamento.tipo());
}
}

Depois — fechado para modificação, aberto para extensão

Seção intitulada “Depois — fechado para modificação, aberto para extensão”

Definimos uma abstração estável e uma implementação por variação. Adicionar um novo meio de pagamento é criar uma nova classe anotada com @Component, sem tocar em nada existente.

public record Pagamento(BigDecimal valor, String documento) {}
public interface MeioPagamento {
boolean suporta(String tipo);
String processar(Pagamento pagamento);
}
@Component
public class PagamentoCartao implements MeioPagamento {
@Override
public boolean suporta(String tipo) {
return "CARTAO".equalsIgnoreCase(tipo);
}
@Override
public String processar(Pagamento pagamento) {
return "Cobrado " + pagamento.valor() + " no cartão";
}
}
@Component
public class PagamentoPix implements MeioPagamento {
@Override
public boolean suporta(String tipo) {
return "PIX".equalsIgnoreCase(tipo);
}
@Override
public String processar(Pagamento pagamento) {
return "Gerado QR Code PIX para " + pagamento.valor();
}
}
@Service
public class ProcessadorPagamento {
private final List<MeioPagamento> meios;
public ProcessadorPagamento(List<MeioPagamento> meios) {
this.meios = meios;
}
public String processar(String tipo, Pagamento pagamento) {
return meios.stream()
.filter(meio -> meio.suporta(tipo))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Tipo não suportado: " + tipo))
.processar(pagamento);
}
}

O Spring injeta automaticamente todas as implementações de MeioPagamento na List. Um novo PagamentoBoleto implements MeioPagamento entra em funcionamento apenas por existir — ProcessadorPagamento permanece fechado.

classDiagram
    class ProcessadorPagamento {
        -List~MeioPagamento~ meios
        +processar(String, Pagamento)
    }
    class MeioPagamento {
        <<interface>>
        +suporta(String) boolean
        +processar(Pagamento) String
    }
    class PagamentoCartao {
        +suporta(String) boolean
        +processar(Pagamento) String
    }
    class PagamentoPix {
        +suporta(String) boolean
        +processar(Pagamento) String
    }
    class PagamentoBoleto {
        +suporta(String) boolean
        +processar(Pagamento) String
    }

    ProcessadorPagamento --> MeioPagamento
    MeioPagamento <|.. PagamentoCartao
    MeioPagamento <|.. PagamentoPix
    MeioPagamento <|.. PagamentoBoleto

A seta de ProcessadorPagamento aponta apenas para a abstração. As implementações novas dependem da abstração, nunca o contrário.

  1. Como resolver a aparente contradição entre “aberto para extensão” e “fechado para modificação”?
Resposta

Por meio de abstração polimórfica: o cliente depende de uma interface estável (fechada), e novos comportamentos entram como novas implementações dessa interface (aberto), sem editar o cliente.

  1. Qual é a heurística de Uncle Bob para não cair em super-engenharia ao aplicar OCP?
Resposta

“Fool me once”: escreva concreto na primeira vez; introduza a abstração quando a segunda variação surgir, pois só então você conhece o eixo real de variação. Antecipar demais viola YAGNI.

  1. Como o Spring viabiliza o OCP no exemplo dos meios de pagamento?
Resposta

Injetando automaticamente todas as implementações da interface MeioPagamento como uma List. Uma nova implementação anotada com @Component passa a funcionar sem alterar ProcessadorPagamento.

  1. Por que OCP depende de LSP?
Resposta

Porque a extensão via polimorfismo só é segura se cada subtipo puder substituir a abstração sem quebrar o contrato esperado pelo cliente. Se um subtipo viola LSP, o cliente “fechado” passa a precisar de tratamento especial, quebrando o OCP.