Motivação
Fala pessoal tranquilo? esse é o segundo texto da série que estou compartilhando minha experiência de forma direta e “pé-no-chão” sobre SOLID. No primeiro texto eu falei sobre SRP e mostrei como pequenas violações atrapalham a manutenção do código, se caso não viu, da uma passada lá!
Hoje vamos dar um passo adiante com o Open–Closed Principle (OCP), o pilar que nos ensina a estender comportamentos sem modificar o que já está funcionando.
Breve resumo
Esse termo foi cunhado por Bertrand Meyer em 1988 e popularizado por Robert C. Martin (Uncle Bob), o OCP é geralmente resumido na frase:
“Entidades de software devem ser abertas para extensão e fechadas para modificação.”
A ideia parece paradoxal, como algo pode ser aberto para extensões e fechado para modificações? O segredo é uma palavrinha: abstrações (interfaces, behaviours, callbacks).
Conforme os sistemas se tornaram maiores em termos de tamanho dos arquivos, numero de arquivos e relação entre eles, se faz necessário utilizar abstrações para expandir os contextos a ponto de que, não é preciso mexer na implementação raiz para adicionar uma modificação, em outras palavras, "ela nunca muda".
Por exemplo - Uma função que calcula um determinado imposto sobre um produto
- O cálculo pode mudar? sim, mas é improvável que mude
- Quais são as variáveis? valor do produto + alíquota
- Quais são as variáveis que podem mudar? alíquota
São exemplos de perguntas que eu me faço para descobrir o ponto de variação da implementação, nesse caso é improvável que o "como fazer" mude, mas é provável que alíquota mude com o tempo.
Pensando em como normalmente se implementaria um calculo assim:
função fazer_calculo(valor_do_produto), faça:
valor_do_produto * @aliquota_imposto ou 0.10
O pseudocódigo acima mostra uma implementação simples e direta de como fazer esse processo, porém existe um problema que é, toda vez que o imposto mudar a gente vai precisar modificar essa função e isso pode gerar problemas em outras pontas do software.
Se invês do código acima, fizessemos isso:
função calcular_aliquota(valor_do_produto, calculo_imposto), faça:
calculo_imposto(valor_do_produto)
Nesse caso não seria necessário modificar essa implementação quando algo mudasse, visto que, estamos passando uma outra função como parâmetro para calcular o novo valor, assim quando adicionarmos novos recursos implementando classes/módulos, não precisamos alterar os contextos já testados no passado, dessa forma expandindo as implementações.
OCP na prática
1. Estratégia de desconto em Go.
Trecho retirado do repositório solid-go-examples:
// interface que define o ponto de variação
type Discount interface {
Apply(price float64) float64
}
// implementação sem desconto
type NoDiscount struct{}
func (NoDiscount) Apply(price float64) float64 { return 0 }
// implementação com desconto percentual
type PercentageDiscount struct{}
func (PercentageDiscount) Apply(price float64) float64 { return price * 0.1 }
// Função de domínio que não muda quando surgem novos descontos
func CalculatePrice(price float64, d Discount) float64 {
return price - d.Apply(price)
}
Uso no main.go:
noDisc := NoDiscount{}
percentageDisc := PercentageDiscount{}
fmt.Println(CalculatePrice(100, noDisc)) // 100
fmt.Println(CalculatePrice(100, percentageDisc)) // 90
Por que segue o OCP?
CalculatePrice
é fechada para modificação: não precisei tocar nela para incluir PercentageDiscount
. Ao mesmo tempo, o sistema está aberto para extensão, pois novos tipos que implementem Discount
podem ser criados a qualquer momento (ex: BlackFridayDiscount).
2. Processador de pagamento em Elixir.
Arquivo 02_open_closed_principle.ex
do repo solid_elixir_examples
:
defmodule PaymentProcessor do
def process(order, tax_calculator) do
tax = tax_calculator.(order)
total = order.amount + tax
{:ok, %{order: order, tax: tax, total: total}}
end
end
# diferentes regras fiscais sem alterar o módulo
PaymentProcessor.process(%{amount: 100}, fn o -> o.amount * 0.1 end)
PaymentProcessor.process(%{amount: 100}, fn o -> o.amount * 0.2 end)
Por que segue o OCP?
PaymentProcessor
delega o cálculo de imposto para uma função recebida por parâmetro. Quando surge uma nova legislação, criamos outra função (ou módulo) e passamos como argumento, sem tocar na lógica central de processamento.
Dicas para adotar o OCP
Situação de mudança | Abordagem que respeita o OCP |
---|---|
Novos meios de pagamento | Defina uma interface PaymentGateway e implemente novos adaptadores sem alterar o que já existe. |
Novas regras de tarifação | Use funções de callback ou strategy pattern para encapsular regras isso vai ajudar na manutenibilidade do código e testes. |
Novos relatórios | Aplique o pattern Template Method ou gere relatórios via plug-ins em vez de editar a classe base. |
Dica de ouro: encontre o ponto de variação do código, provávelmente esse é o ponto que precisa ser encapsulado.
Sinais de violação
-
switch/case
crescendo sempre que aparece um novo tipo de dado. - Testes que quebram em cascata quando apenas um requisito mudou.
- Mudança constante no domínio interno (quanto mais mudanças em regras internamente tiver mais se faz necessário aplicar esse conceito).
Conclusão
Aplicar o Open–Closed Principle não é sobre “nunca mais mexer” no código, mas sim sobre minimizar mudanças de alto risco e isolar variabilidade. Quanto mais clara for a fronteira entre núcleo e extensões, mais rápido e seguro será evoluir o sistema.
No próximo artigo vamos explorar o Liskov Substitution Principle (LSP). Enquanto isso, confira os repositórios completos com exemplos em Elixir e Go:
- Exemplo em Elixir: solid_elixir_examples
- Exemplo em Go: solid-go-examples
Se ainda houver dúvidas ou se você discordar de algo, deixe um comentário!
Top comments (0)