Fala, galera

Voltando à série sobre os princípios SOLID iniciada <aqui>, vamos desta vez com o princípio 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.

Espero que tenham gostado.

Até a próxima!
—————————–

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)