Princípios SOLID: Princípio do Aberto/Fechado (OCP)

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)

25 comentários em “Princípios SOLID: Princípio do Aberto/Fechado (OCP)

Adicione o seu

  1. Saberia me dizer se é possivel conseguir o livro Agile Principles, Patterns, and Practices in C# sem ser na amazon?

    Curtir

      1. 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

        Curtir

  2. 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.

    Curtir

    1. 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

      Curtir

  3. 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

    Curtir

    1. 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

      Curtir

  4. 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();
    }
    }

    Curtir

    1. 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:

      public class MeuAdapterProGeradorFechado : IGeradorDeArquivo
      {
          private readonly GeradorAdaptado _geradorAdaptado;
      
          public MeuAdapterProGeradorFechado(GeradorAdaptado geradorAdaptado)
          {
              _geradorAdaptado = geradorAdaptado;
          }
      
          public void Gerar(int codigo)
          {
              _geradorAdaptado.Gerar(codigo, "nome-dummy");
          }
      }
      

      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

      Curtir

Participe! Vamos trocar uma ideia sobre desenvolvimento de software!

Blog no WordPress.com.

Acima ↑