OCP (Open-Close Principle): Uma Abordagem Flexível
Introdução ao OCP
Open Close Principle (OCP) é a segunda letra do acrônimo SOLID (O) e se resume a “aberto para extensão, fechado para modificação”. Basicamente, significa que uma vez que o módulo (package, class, method, etc.) esteja em ambiente produtivo, não deveríamos modificá-lo diretamente, mas sim estendê-lo para adicionar novos comportamentos ou features. Na orientação a objetos, utilizamos herança, interfaces e abstrações para respeitar o OCP.
Uma classe que é modificada frequentemente pode trazer sérios prejuízos para a manutenibilidade. Além de ter o risco de alterar um comportamento existente em produção (bugs), podemos ter conflitos entre mudanças (conflitos de merges). É muito comum definir um contrato básico via interface a fim de possibilitar maior flexibilidade e extensibilidade, além de se relacionar diretamente com Dependency Inversion Principle (DIP).
OCP em Código
Um exemplo muito comum de violação do OCP em nível de classe é a criação de IF statements para controlar um fluxo que pode sofrer mudanças diretas ao adicionar novas features. Mesmo em códigos profissionais e enterprise, o exemplo abaixo é comum:
1
2
3
4
5
6
7
8
9
10
11
public class FormaPagamento {
public boolean processarPagamento(TipoPagamentoEnum tipoPagamento, PagamentoDadosDto dados) {
if (tipoPagamento.CREDITO.equals(tipoPagamento)) {
// codigo para processar credito
} else if (tipoPagamento.DEBITO.equals(tipoPagamento)) {
// codigo para processar debito
} else {
throw new RuntimeException("Tipo de pagamento não localizado");
}
}
}
Acima, vemos que a classe FormaPagamento
possui um if statement para cada forma de pagamento. Imagine que uma nova feature precisa ser adicionada a esse módulo e, nessa feature, precisamos adicionar uma nova forma de pagamento: PIX. Para essa feature, precisaríamos alterar a classe que já está em produção, sendo assim uma violação ao OCP.
Existem algumas maneiras de resolver esse problema, e o mais comum é utilizar o Design Pattern (DP) Strategy.
É aconselhável sempre desenhar a solução antes de aplicá-la em código para sua validação.
No diagrama acima, a classe PagamentoContext
utiliza a interface PagamentoStrategy
. Nesse caso, ela não conhece os detalhes de implementação de pagamento, apenas chama o método processarPagamento
.
Implementação em Código
Abaixo, você encontrará a implementação do padrão Strategy para resolver o problema descrito:
Interface define o contrato básico que todos os novos Planos de Pagamento precisam seguir.
Sempre que uma nova forma de pagamento é adicionada, implementamos PagamentoStrategy
, respeitando o OCP.
1
2
3
public interface PagamentoStrategy<V> {
boolean processar(V dados);
}
Exemplos de implementações para cada forma de pagamento:
1
2
3
4
5
public class PagamentoDebito implements PagamentoStrategy<DadosDebito> {
boolean processar(DadosDebito dados) {
// codigo para processar debito
}
}
1
2
3
4
5
public class PagamentoCredito implements PagamentoStrategy<DadosCredito> {
boolean processar(DadosCredito dados) {
// codigo para processar credito
}
}
1
2
3
4
5
public class PagamentoPix implements D<DadosPix> {
boolean processar(DadosPix dados) {
// codigo para processar PIX
}
}
Também poderíamos implementar um Factory para criação dos Strategies, mas nesse caso, o Context cuidará dessa parte.
1
2
3
4
5
6
7
8
9
10
11
public class PagamentoContext<V> {
private final PagamentoStrategy<V> strategy;
public PagamentoContext(PagamentoStrategy<V> strategy) {
this.strategy = strategy;
}
public boolean processarPagamento(V dadoPagamento) {
return strategy.processar(dadoPagamento);
}
}
A classe PagamentoContext
centraliza o uso do strategy, trazendo alguns benefícios como, por exemplo, a possibilidade de adicionar comportamento Pre e Pós a execução do Strategy.
Exemplo de uso
1
2
3
4
5
6
7
8
9
10
11
12
package org.example;
import org.example.model.DadosCredito;
public class App {
public static void main(String[] args) {
DadosCredito dadosCredito = new DadosCredito();
PagamentoContext<DadosCredito> creditoPagamentoContext = new PagamentoContext<>(new PagamentoCredito());
boolean result = creditoPagamentoContext.processarPagamento(dadosCredito);
System.out.println("resultado: " + result);
}
}
Conclusão
O Open Close Principle é fundamental para criar sistemas robustos e manuteníveis. Ao seguir este princípio, garantimos que as classes existentes permaneçam fechadas para modificação, enquanto permanecem abertas para extensão. A aplicação do padrão Strategy, como demonstrado, é uma maneira eficaz de aderir ao OCP, permitindo a adição de novas funcionalidades sem alterar o código existente. Isso não apenas melhora a qualidade do código, mas também facilita a colaboração em projetos de equipe, minimizando conflitos e bugs. A abordagem orientada a objetos, quando bem aplicada, pode levar a um design de software mais limpo, eficiente e sustentável.