DEV Community

Cover image for Entendendo SOLID de uma vez por todas | Parte 02 - (OCP)
Rafael Honório
Rafael Honório

Posted on

Entendendo SOLID de uma vez por todas | Parte 02 - (OCP)

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

Uso no main.go:

noDisc := NoDiscount{}
percentageDisc := PercentageDiscount{}

fmt.Println(CalculatePrice(100, noDisc)) // 100
fmt.Println(CalculatePrice(100, percentageDisc)) // 90
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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:

Se ainda houver dúvidas ou se você discordar de algo, deixe um comentário!

Top comments (0)