Princípio Aberto/Fechado
Esse é o segunda pilar do SOLID. Sua fundamentação existe para que os softwares criados sejam fáceis de manter e evoluir sem quebrar o que já funciona.
Certo! Mas o que diabos isso significa? Os sistemas não são estáticos, eles tendem a evoluír. O cenário comum no mercado, portanto, é: com novas definições de funcionalidades os desenvolvedores geralmente alteram o código-fonte existente, código este, que já foi testado e homologado. Resultando em um sistema difícil de manter e “sustentar”.
O OCP surge para mitigar esse problema e a definição foi criada por Bertrand Meyer:
"Entidades de software (classes, módulos, funções, etc.) devem estar abertas para extensão, mas fechadas para modificação."
- Aberto pra Extensão: Você é deve ser capaz de adicionar novos comportamentos e funcionalidades ao sistema à medida que os requisitos mudam.
- Fechada para Modificações: Ao adicionar essa nova funcionalidade, você não deve alterar o código-fonte existente que já foi estado e está funcionando
Metáfora da Tomada
Tenho um professor que comparou esse princípio com uma tomada. elétrica de parede.
- Ela é fechada pra modificação, já que você não precisa quebrar a parede e refazer a fiação elétrica da casa toda vez que compra um novo eletrodoméstico
- Ela é abertura para extensão, você pode instalar uma TV, ventilador ou carregador estendendo a utilidade daquela rede relétrica, apenas plugando algo novo. A interface (os buracos da tomada) permitee isso.
Exemplo prático
Vamos desenvolver um cálculo de Bônus Anual. Imagine uma classe CalculadoraDeBonus. Inicialmente, a empresa só tem "Desenvolvedores" e "Gerentes".
Exemplo Ruim (Violando OCP)
Nesta abordagem comum (mas ruim), usamos um switch ou if/else para verificar o cargo. O problema: quero adicionar dois novos cargos, diretor e estagiário. Para isso eu preciso abrir a classe e modificá-la diretamente! Isso viola o OCP
public class CalculadoraDeBonus
{
public decimal Calcular(string cargo, decimal salario)
{
if (cargo == "Gerente")
{
return salario * 0.2m;
}
else if (cargo == "Desenvolvedor")
{
return salario * 0.1m;
}
// Se a empresa criar o cargo "Diretor" amanhã,
// VOU TER QUE ALTERAR ESTE ARQUIVO.
return 0;
}
}
O Risco: Ao editar esse arquivo para adicionar o "Diretor", você pode sem querer apagar uma linha ou mudar a lógica do "Gerente", causando bugs em algo que já estava testado.
Solução (Aplicando OCP)
Vou trazer duas abordagens para fechar a classe para modificação e abri-las para extensão. Ambas abordagens resolvem o OCP, mas elas comunicam intenções diferentes sobre o seu sistema.
- Abordagem com Classe Abstrata
- Abordagem com Interfaces
1. Abordagem com Classe Abstrata
Use essa abrodagem quando existe uma relação forte de herança e você quer reaproveitar propriedades ou métodos
Cenário: Todo funcionário tem Nome e Salario (código compartilhado), mas o cálculo do bônus muda.
Contrato
public abstract class FuncionarioBase
{
// Código compartilhado (Reuso)
public string Nome { get; set; }
public decimal Salario { get; set; }
// Obriga as classes filhas a implementarem a lógica específica
public abstract decimal CalcularBonus();
// Método comum que já funciona para todos (não precisa sobrescrever)
public string ObterCracha() => $"Funcionário: {Nome}";
}
Aberto para extensões:
public class Gerente : FuncionarioBase
{
public override decimal CalcularBonus()
{
return Salario * 0.2m;
}
}
public class Desenvolvedor : FuncionarioBase
{
public override decimal CalcularBonus()
{
return Salario * 0.1m;
}
}
Uso
public void Processar(FuncionarioBase funcionario)
{
Console.WriteLine(funcionario.CalcularBonus());
}
2. Abordagem com Interface
Use esta abordagem quando o foco é o "O QUE FAZ" (Comportamento). Interfaces são contratos puros. Elas não carregam "bagagem" (estado ou variáveis) e permitem maior flexibilidade, pois uma classe pode implementar múltiplas interfaces.
Cenário: Aqui não nos importamos se é um funcionário, um prestador de serviço ou um robô. Só nos importamos se ele tem uma regra de bônus. Isso é frequentemente chamado de Strategy Pattern.
Contrato
// --- O Contrato (Interface) ---
public interface IRegraDeBonus
{
decimal Calcular(decimal salarioBase);
}
public class BonusParaDesenvolvedor : IRegraDeBonus
{
public decimal Calcular(decimal salarioBase)
{
return salarioBase * 0.1m;
}
}
Aberto para extensões:
// Note que não herdamos de ninguém, apenas assinamos o contrato
public class BonusParaGerente : IRegraDeBonus
{
public decimal Calcular(decimal salarioBase)
{
return salarioBase * 0.2m;
}
}
Uso
public class CalculadoraServico
{
// Recebemos a INTERFACE. Baixíssimo acoplamento.
public decimal ExecutarCalculo(decimal salario, IRegraDeBonus regra)
{
return regra.Calcular(salario);
}
}
Comparação entre abordagens: Abstract Class vs. Interface
| Característica | Abstract Class | Interface |
|---|---|---|
| Relacionamento | "É um" (Hierarquia estrita). Um Gerente é um Funcionário. | "Faz isso" (Capacidade). Essa classe sabe calcular bônus. |
| Reuso de Código |
Alto. Permite compartilhar propriedades (Nome, ID) e métodos concretos. |
Baixo/Nenhum. Apenas define as assinaturas dos métodos.* |
| Herança | Limitada. C# só permite herdar de 1 classe. Se usar Abstract, você "gasta" essa cartada. |
Múltipla. Uma classe pode implementar 5 interfaces diferentes (IBonificavel, IAutenticavel, etc). |
| Acoplamento | Médio. As filhas dependem da classe pai. | Baixíssimo. As classes só dependem do contrato. |
| Quando usar no OCP? | Quando você tem uma família de objetos intimamente relacionados que compartilham estrutura. | Quando você quer plugar comportamentos diferentes (Strategy Pattern) ou testabilidade máxima (Mocks). |
No desenvolvimento moderno de C# (.NET Core/Dependency Injection), Interfaces são usadas em 90% dos casos para garantir o OCP, pois facilitam muito os testes unitários (Mocking).
Top comments (0)