Voltando à série sobre os princípios SOLID iniciada <aqui>, vamos desta vez com o Open-Closed Principle (Princípio do Aberto/Fechado), ou simplesmente, OCP.
O OCP é mais um daqueles princípios de orientação a objetos que nos ajudam a eliminar design smells, possibilitando que nosso código ganhe em facilidade de manutenção e extensão.
DEFINIÇÃO
“Entidades de software (classes, módulos, funções, etc.) devem ser abertas para extensão mas fechadas para modificação.”
A moral da história é a seguinte: quando eu precisar estender o comportamento de um código, eu crio código novo ao invés de alterar o código existente.
E como isso é possível? Como adicionar comportamentos novos sem alterar o código existente? Isso mesmo: ABSTRAÇÃO!
Vamos clarear as coisas com um exemplo.
EXEMPLO DE VIOLAÇÃO
Vamos considerar o seguinte:
public class Arquivo { } public class ArquivoWord : Arquivo { public void GerarDocX() { // codigo para geracao do arquivo } } public class ArquivoPdf : Arquivo { public void GerarPdf() { // codigo para geracao do arquivo } } public class GeradorDeArquivos { public void GerarArquivos(IList<Arquivo> arquivos) { foreach(var arquivo in arquivos) { if (arquivo is ArquivoWord) ((ArquivoWord)arquivo).GerarDocX(); else if (arquivo is ArquivoPdf) ((ArquivoPdf)arquivo).GerarPdf(); } } }
O exemplo acima é bem claro, certo? Temos classes que geram arquivos do Word e PDFs. E temos uma classe “GeradorDeArquivos” que recebe uma lista de arquivos e gera todos eles (por “gerar”, entenda criar um arquivo novo no formato especificado e salvá-lo em disco).
Suponha agora que tenhamos que estender a aplicação para dar suporte a arquivos em outro formato, como, por exemplo, arquivos texto (.txt) e precisamos que o método “GerarArquivos” também gere arquivos no novo formato.
Além da nova classe, que poderíamos chamar de ArquivoTxt, seríamos obrigados a alterar o método “GerarArquivos” para atender a esse requisito. O mais óbvio seria colocar mais um “else if”, checando pelo novo tipo (txt) e chamando o método correspondente: ((ArquivoTxt)arquivo).GerarTxt(). Esse padrão seguiria sucessivamente a cada necessidade de um novo formato de arquivo.
Sendo assim, podemos afirmar que o método “GerarArquivos” não está em conformidade com o OCP para mudanças do tipo “preciso de um novo formato de arquivo”, uma vez que o método não está fechado para essas mudanças.
PROBLEMAS
Você pode estar se perguntando: qual o problema de mais um simples “else if”?
Vamos pensar nas seguintes situações:
- Existem outras partes da aplicação que também fazem as verificações por tipo vistas no método “GerarArquivos” para invocar outros métodos específicos de cada classe concreta.
- Para piorar, algumas dessas partes estão em outros componentes da aplicação.
O que acontece quando precisamos de um novo formato de arquivo?
Além de criarmos nosso novo arquivo, como “ArquivoTxt”, teríamos que:
1) Alterar todos os métodos que precisem fazer uso do novo formato (certamente aqueles com vários “if/else if” ou um belo “switch..case”).
2) Recompilar e fazer o deploy de todos os componentes que foram impactados.
Quando uma mudança dessas acaba causando uma série de mudanças em cascata, fica claro que nosso design não está bom pois, além de mais trabalho para alterarmos, ainda podemos nos esquecer de algumas dessas partes do código.
Além disso, quanto mais código para alterar, que já estava pronto e funcionando, mais chances de introduzir bugs (espero que seu código esteja coberto por testes!).
ATENDENDO OCP
Vejamos como fica o código alterado para atender o OCP:
public abstract class Arquivo { public abstract void Gerar(); } public class ArquivoWord : Arquivo { public override void Gerar() { // codigo para geracao do arquivo } } public class ArquivoPdf : Arquivo { public override void Gerar() { // codigo para geracao do arquivo } } public class ArquivoTxt : Arquivo { public override void Gerar() { // codigo para geracao do arquivo } } public class GeradorDeArquivos { public void GerarArquivos(IList<Arquivo> arquivos) { foreach(var arquivo in arquivos) arquivo.Gerar(); } }
Fizemos pequenas mudanças:
1) Tornamos “Arquivo” uma classe abstrata, uma vez que não temos intenção de instanciá-la.
2) Criamos um método abstrato para geração de arquivos na classe base (chamado de “Gerar”).
3) Fizemos com que as classes derivadas implementem o método “Gerar”.
4) Introduzimos nosso novo requisito, ou seja, um novo tipo de arquivo (ArquivoTxt), o qual também herda de “Arquivo” e implementa “Gerar”.
5) Por fim, eliminamos as checagens de tipo do método “GerarArquivos” e passamos a usar polimorfismo.
Retomando a moral da história lá do comecinho do post:
Agora, sempre que surgir um novo formato de arquivo, nós conseguimos estender o comportamento de “GerarArquivos” (ele saberá gerar esse novo arquivo) sem precisarmos alterá-lo. Apenas criamos o arquivo novo e pronto. Nada mais a fazer!
OBSERVAÇÕES
1) No exemplo que mostrei, o OCP é atendido para mudanças do tipo “surgiu um novo formato de arquivo”. Isso quer dizer que o método “GerarArquivos” está fechado para esse tipo de mudança, mas que há possibilidade de que tenhamos que alterá-lo caso surja alguma mudança de outro tipo.
2) Fica claro então, pelo item 1, que uma classe nunca estará totalmente fechada para todo tipo de mudança possível. Nunca iremos conseguir cobrir todos os cenários possíveis. Mesmo que tenhamos experiência, que conheçamos bem o negócio em questão, nunca conseguiremos prever tudo.
3) Por consequência do item 2, devemos ter cautela ao criar abstrações e não termos um surto de sair abstraindo tudo. (Falei sobre isso em um dos posts passados: “Abstrair (mas em pontos estratégicos)”. Dê uma lida.)
CONCLUSÃO
O Princípio do Aberto/Fechado nos atenta para a aplicação de abstrações e polimorfismo, de forma consciente, garantindo que tenhamos um software mais flexível e, portanto, mais fácil de ser mantido.
É mais um dos bons princípios de orientação a objetos que devemos ter em nosso cinto de utilidades.
—————————–
Toda a série:
Princípio da Responsabilidade Única (SRP)
Princípio do Aberto/Fechado (OCP)
Princípio da Substituição de Liskov (LSP)
Princípio da Segregação de Interface (ISP)
Princípio da Inversão de Dependência (DIP)
Quando vejo OCP, sempre lembro do Robocop ehehehhehehe
Legal a abordagem e a escrita. Abraços
CurtirCurtir
Nerd!!
Valeu. []s
CurtirCurtir
Bem escrito,clara e objetiva. Isso ae mestre obi-wan kenobi!! []’s
CurtirCurtir
Obrigado, cara
[]s
CurtirCurtir
Muito bom sr. @nosborcastilho. Muito bem explicado. Simples e objetivo. Nunca mais cometerei erros que violem o OCP (rsrsrs). Congratulations!!!
CurtirCurtir
É isso aí, estender a capacidade da rede, sem alterar o que já existe!
[]s
CurtirCurtir
Saberia me dizer se é possivel conseguir o livro Agile Principles, Patterns, and Practices in C# sem ser na amazon?
CurtirCurtir
Olá, Conrado
Falaram pra mim deste site há uns dias:
http://www.grupoa.com.br/livros/engenharia-de-software-e-metodos-ageis/principios-padroes-e-praticas-ageis-em-c/9788577808410
Não sei da procedência e o preço é meio carinho (sem contar o frete).
Em outras lojas mais conhecidas, nunca pesquisei.
[]s
CurtirCurtir
Obrigado Robson,
Sem querer fugir muito do assunto, mas apenas para tirar a duvida mesmo.
Esse site que você passou o livro esta traduzido em português, já o da Amazon é em inglês.
Certo?
Abraço
CurtirCurtir
Exato ( e não sei o nível da tradução ).
Normalmente compro em inglês.
[]s
CurtirCurtir
Open-Closed só é possível se caso você acompanhe a titia Liskov, que acho que é o próximo tópico ;)~ […]
E esta foi mais uma dose de bom senso nesse Matagal Grosso (desvairado) do Sul.
CurtirCurtir
Yes..na fila..
CurtirCurtir
Mais um artigo simples e objetivo! Aguardo as aulas sobre os 3 princípios restantes.
CurtirCurtir
Em andamento!
Valeu
[]s, camarada
CurtirCurtir
Boa tiozão, ótima explicação!
Poderia dizer que um bom exemplo disso seriam os nossos States?
Abs.
CurtirCurtir
Sim. Quando surge um novo estado, os métodos que utilizam o estado abstrato não precisam de alteração (fechados) porém ganham comportamento extra, graças ao estado novo (abertos para extensão).
O mesmo vale para todos os métodos que fazem uso de uma questão abstrata. Surgindo um tipo novo de questão (ou até mesmo excluindo um tipo existente) esses métodos não precisarão de alteração.
Pela magia da abstração e do polimorfismo!
[]s
CurtirCurtir
Pela magia da abstração e do polimorfismo? E eu aqui jurando que era pela magia do nitrato de amônia 😦 (haha)
Muito bom!
CurtirCurtir
Essa é pros casos mais difíceis hehe
[]s
CurtirCurtir
Boa Robson!
Duvidas:
Por que a classe abstrata não poderia ser uma interface?
A classe abstrata está fechada para extensão, mas ela também não faz nada, que proveito tem em fazer ela existir? Se o programador não quiser herdar dela e fazer uma classe solta, não daria no mesmo mas eu também não consigo aproveitar nada da classe abstrata, acho ela mais parecida com um contrato.
Abraços
CurtirCurtir
Olá, Everson
Sim. Pode ser uma interface perfeitamente. Não tive nenhum motivo específico para escolher a classe abstrata ao invés da interface. Foi só pro exemplo.
Se a relação é de ‘é-um’ e há dados e/ou comportamento comum, isso estará em uma classe abstrata. Ou seja, classes abstratas caem bem, por exemplo, como layer super-types, oferecendo serviços comuns para suas derivadas (como um RepositorioBase).
Se é um ‘faz-isso’, há possibilidade de usá-la com classes diversas (sem nenhuma relação entre elas) e será de uso de outra camada, de modo a manter baixo acoplamento, então está mais pra interface.
Enfim, a escolha entre um tipo ou outro vai depender de cada situação.
[]s
CurtirCurtir
Robson, estou aprendendo muito com seus artigos. Parabéns.
Me surgiu uma dúvida.
E se em cada classe derivada que implementasse o método Gerar(), esse método tivesse parâmetros diferentes em tipos e quantidades? Qual seria a melhor solução?
public abstract class Arquivo
{
public abstract void Gerar();
}
public class ArquivoWord : Arquivo
{
public override void Gerar(int Codigo)
{
// codigo para geracao do arquivo
}
}
public class ArquivoPdf : Arquivo
{
public override void Gerar(string Nome)
{
// codigo para geracao do arquivo
}
}
public class ArquivoTxt : Arquivo
{
public override void Gerar(char letra, int codigo)
{
// codigo para geracao do arquivo
}
}
public class GeradorDeArquivos
{
public void GerarArquivos(IList arquivos)
{
foreach(var arquivo in arquivos)
arquivo.Gerar();
}
}
CurtirCurtir
Olá, Leonardo
Que bom que os posts estão sendo úteis! Obrigado!
Quanto à sua dúvida, vamos pensar ao contrário. A abstração deveria nascer de um conjunto de classes concretas que tem uma API semelhante. Sendo assim, você deveria trabalhar para que todas elas ficassem com a mesma assinatura do método Gerar, por exemplo, todas com Gerar(int codigo). Só DEPOIS disso é que você criaria a abstração e eliminaria a dependência concreta da classe que a utiliza. Então, você precisa verificar se não há como refatorar, se não há um problema de design em alguma delas, para poder equiparar as assinaturas dos métodos.
Se realmente não há como, é possível que elas (ou parte delas) não devam herdar/implementar a mesma classe/interface.
Existem outras alternativas mas precisaria saber qual é o seu contexto. Só pra dar um exemplo, suponha que a interface possua Gerar(int codigo) e você tenha 4 geradores que a implementem e um quinto gerador onde a assinatura seja diferente (Gerar(int codigo, string nome)) MAS você não pode alterar o código fonte dele. Nesse caso, você pode usar o padrão Adapter para envolver o quinto gerador:
No caso acima, você ‘forçou’ que ele implemente a interface (na verdade, o Adapter é que a implementa e o adaptado é usado dentro dele). É uma solução quando você quer usar todos eles polimorficamente e o parâmetro “nome” do quinto gerador (o adaptado) não serve pra nada.
Espero que tenha esclarecido.
Qquer coisa, fala aí!
[]s
CurtirCurtir
Cara, mandou muito bem!
Ficou muito de entender um conceito complexo com sua didática.
Parabéns.
CurtirCurtir
Muito obrigado, Weslei!
Que bom que ajudei.
[]s
CurtirCurtir