Aplicando SOLID com C# — OCP

Dando continuidade ao estudo sobre os padrões SOLID, no primeiro artigo falamos sobre o SRP, agora na segunda parte, vamos conversar um pouco sobre OCP (Open Closed Principle), ou em português (Princípio Aberto-Fechado).

O que se trata?

Como o nome já sugere, o padrão OCP diz que uma classe ou método durante o desenvolvimento deve estar aberto para extensão e fechado para modificação quando necessário uma adição de funcionalidade, ou seja, o desenvolvedor visa diminuir a complexidade e riscos em manutenções futuras. Eu diria que o OCP trabalha junto com o SRP, um complementa o outro, para implementar se faz uso de heranças e polimorfismo, pilares da programação orientada a objeto.

Legal, sabendo disso, vamos brincar um pouco e simular um contexto real, nada daqueles exemplos com animais, formas geométricas e etc. Nosso objetivo é construir um sistema de aluguel de carros onde vou mostrar um cenário ruim e em seguida vamos refatorar e entregar um cenário bom.

Especificação:

Em nosso sistema de teste, o usuário vai informar qual a categoria de carro e quantos dias deseja ficar com o mesmo, conforme essas informações vamos calcular o valor do aluguel, beleza?

Vamos começar, crie uma aplicação com a linguagem Visual C# do tipo Console Application, dê o nome que achar conveniente.

Crie um arquivo do tipo class com o nome TipoCarro e o mesmo deve ficar como na Listagem 01

[code language=”csharp”]

public enum TipoCarro
 {
 Hatch = 1,
 Sedan = 2
 }

[/code]

Listagem 01 — Enumeração indicando os tipos de carros

Agora adicione uma nova classe chamada AluguelBusiness e o código deverá ficar como na Listagem 02

[code language=”csharp”]

public class AluguelBusiness
 {
 public double ObterValorAluguel(DateTime dataEntrega, TipoCarro tipoCarro)
 {
 /*Conforme a categoria do carro, o valor da diária é cobrado
 os modelos Hatch sai por 30 reais e os modelos Sedan por 50 reais*/
 switch (tipoCarro)
 {
 case TipoCarro.Hatch:
 return (dataEntrega.Subtract(DateTime.Today).TotalDays) * 30;
 case TipoCarro.Sedan:
 return (dataEntrega.Subtract(DateTime.Today).TotalDays) * 50;
 default:
 return 0;
 }
 }
 }
[/code]

Listagem 02

Essa regra de negócio será chamada na classe Program.cs, veja na Listagem 03.

[code language=”csharp”]
 static void Main(string[] args)
 {
 AluguelBusiness aluguelBO = new AluguelBusiness();
 var valorAPagar = aluguelBO.ObterValorAluguel(DateTime.Today.AddDays(2), TipoCarro.Sedan);

Console.WriteLine(String.Format(“Carro de categoria: {0} — Valor a Pagar: R$ {1}”, TipoCarro.Sedan.ToString(), valorAPagar));
 Console.ReadLine();
 }
[/code]

Listagem 03 — Classe Program.cs

Se executarmos o projeto, o valor do Sedan por 2 dias conforme nossa regra de negócio, será de R$ 100,00…tudo perfeito, mas o que há de errado nesse código? Por enquanto tudo lindo…

Dai chegou o grande dia que nossa empresa vai trabalhar com aluguel de automóveis de categoria SUV e o valor da diária será de R$ 100,00, nosso sistema terá que contemplar essa regra, vamos implementar?

É aí que surge o problema…

A classe AluguelBusiness está construída e testada, funciona perfeitamente, porém para desenvolver a nova funcionalidade, deparamos com o problema que a mesma não está aberta para extensão e sim para modificação. Agora temos a obrigação de programar internamente no método ObterValorAluguel adicionando um novo comportamento e correr o risco de algo que funcionava parar.

Antes de mais nada, modificamos a enumeração com os tipos. Acompanhe na Listagem 04.

Como funciona na prática?

[code language=”csharp”]
 public enum TipoCarro
 {
 Hatch = 1,
 Sedan = 2,
 SUV = 3 /*Nova categoria de carro adicionada*/
 }
[/code]

Listagem 04

1 — Implementação Ruim

[code language=”csharp”]

public double ObterValorAluguel(DateTime dataEntrega, TipoCarro tipoCarro)
{
 /*Adicionado mais uma condicional ao switch e dentro mais regras de negócio*/
 switch (tipoCarro)
 {
 case TipoCarro.Hatch:
 return (dataEntrega.Subtract(DateTime.Today).TotalDays) * 30;
 case TipoCarro.Sedan:
 return (dataEntrega.Subtract(DateTime.Today).TotalDays) * 50;
 case TipoCarro.SUV:
 return (dataEntrega.Subtract(DateTime.Today).TotalDays) * 100;
 default:
 return 0;
 }
}

[/code]

Listagem 05

Vai funcionar? Claro que vai, mas veja que a classe ainda está aberta para modificações internas e praticamente nula a extensibilidade. Além disso, está esteticamente feia já que métodos com muitas condições de switch ou de if/else tornam-se muito complexos para entendimento e manutenção. Esses fatores faz com que todos os testes já realizados tenham que novamente ser feitos, além dos testes da nova funcionalidade deixando todo o processo mais custoso e arriscado a boas horas extras…

2 — Implementação Boa

Agora vamos a implementação que cheguei usando o padrão OCP para tornar essa classe fechada internamente e aberta para extensão, vamos lá!

Na listagem abaixo coloquei os novos objetos para cumprir conforme o padrão OCP.

[code language=”csharp”]

/*Primeiro usei uma classe abstrata para o uso do polimorfismo*/
 public abstract class AluguelBusinessOCP
 {
 public abstract bool Tipo(TipoCarro tipoCarro);

public abstract double ObterValorAluguel(DateTime dataEntrega);
 }
[/code]

Listagem 06

Agora cada categoria de carro terá a sua própria implementação de como se deve calcular o preço do aluguel, veja na Listagem 07.

[code language=”csharp”]

/*Especialização de Tipo Hatch*/
public class AluguelHatchBusinessOCP : AluguelBusinessOCP
{
 public override bool Tipo(TipoCarro tipoCarro)
 {
 return tipoCarro == TipoCarro.Hatch;
 }

public override double ObterValorAluguel(DateTime dataEntrega)
 {
 return (dataEntrega.Subtract(DateTime.Today).TotalDays) * 30;
 }
}

/*Especialização de Tipo Sedan*/
public class AluguelSedanBusinessOCP: AluguelBusinessOCP</pre>
{
 public override bool Tipo(TipoCarro tipoCarro)
 {
 return tipoCarro == TipoCarro.Sedan;
 }

public override double ObterValorAluguel(DateTime dataEntrega)
 {
 return (dataEntrega.Subtract(DateTime.Today).TotalDays) * 50;
 }
 }

/*Especialização de Tipo SUV*/
public class AluguelSUVBusinessOCP : AluguelBusinessOCP
{
 public override bool Tipo(TipoCarro tipoCarro)
 {
 return tipoCarro == TipoCarro.SUV;
 }
 public override double ObterValorAluguel(DateTime dataEntrega)
 {
 return (dataEntrega.Subtract(DateTime.Today).TotalDays) * 100;
 }
}

[/code]

Listagem 07

Veja como ficou a classe AluguelBusiness na Listagem abaixo.

[code language=”csharp”]
public class AluguelBusiness
{
 private List<AluguelBusinessOCP> _tiposCarro;

public AluguelBusiness()
 {
 _tiposCarro = new List<AluguelBusinessOCP>();
 _tiposCarro.Add(new AluguelHatchBusinessOCP());
 _tiposCarro.Add(new AluguelSUVBusinessOCP());
 _tiposCarro.Add(new AluguelSedanBusinessOCP());
 }

public double ObterValorAluguel(DateTime dataEntrega, TipoCarro tipoCarro)
 {
 var tipoEscolhido = _tiposCarro.Find(x => x.Tipo(tipoCarro));
 return tipoEscolhido.ObterValorAluguel(dataEntrega);
 }
}
[/code]

Listagem 08

Veja que a assinatura do método ObterValorAluguel não sofreu nenhuma mudança e conseguimos estender o seu comportamento com a abstração AluguelBusinessOCP. Um ponto que particularmente acho bem legal, é que eliminamos o switch ou if/else. Se amanhã nosso sistema precisar trabalhar com carros do tipo PickUp por exemplo, basta criar mais uma classe herdando a abstração AluguelBusinessOCP, ou seja, a classe AluguelBusiness está aberta para extensão e fechada para modificação conforme o padrão OCP.

Mesmo com toda a refatoração, a chamada da classe de negócio lá no Program.cs permanece a mesma, entenda como se fosse um site ASP.NET MVC, um outro serviço ou um client mobile.

No meu GitHub está disponível um projeto bem semelhante ao do exemplo com adição de escolha do carro a ser alugado.

Projeto no GitHub

Até o próximo artigo sobre SOLID, abraço!

Referências:

OOP — O princípio Open-Closed (OCP)
http://www.macoratti.net/11/05/oop_opc1.htm

Open/closed principle
https://en.wikipedia.org/wiki/Open/closed_principle

SOLID: Padrões flexíveis para suas classes C#
http://www.devmedia.com.br/solid-padroes-flexiveis-para-suas-classes-c/31046