0

Desbravando princípios SOLID: Open/Closed

José Júnior
José Júnior

No mundo da engenharia de software é comum viver situações nas quais aquilo que já foi codificado, testado e entregue como um pacote funcional, precise ser alterado posteriormente quando novas features forem solicitadas. Embora isso faça parte do jogo, essas ondas de alterações no código sempre são potenciais causadoras de bugs, dependendo de como elas estão sendo feitas.

É para mitigar ao máximo este possível epicentro de problemas, evitando modificações em códigos já finalizados, que surge o segundo princípio SOLID:

Open/Closed (Aberto/Fechado)

Uma classe deve estar apta a estender seus comportamentos sem que, para isso, haja a necessidade de modificação em suas implementações.

Apesar do nome por si só não dizer muito a respeito do que se trata, definitivamente ele leva consigo as duas palavras-chaves para início do entendimento do princípio, afinal a regra é sobre deixar os comportamentos de uma classe abertos para acolher no futuro novas features relacionadas, de maneira que não seja necessário alterar as suas implementações para isso. Em outras palavras, o princípio visa deixar códigos previamente preparados para expandirem a gama de seus possíveis resultados de execução, mesmo permanecendo fechados para modificações.

Para construção de um exemplo simples, a fim de facilitar o início do entendimento, considere a classe Usuario:

class Usuario{
   
    String nomeUsuario;
    Perfil perfilUsuario;
}


Agora, a classe Perfil:

class Perfil{
    
    String nomePerfil;
}


A seguir então consta o exemplo da classe ArquivoServiceviolando o princípio de estar aberto para extensão e fechado para modificações:

class ArquivoService{

    public String obterDiretorioPermitidoPorUsuario(Usuario usuario){
        
        if (usuario.perfilUsuario.nomePerfil == "aluno"){
            return "../arquivos-exclusivos/usuario-aluno/";
        }
         
        if (usuario.perfilUsuario.nomePerfil == "professor"){
            return "../arquivos-exclusivos/usuario-professor/";
        }
    }

    
    {...} //outros metodos

}


Do jeito que o método está implementado, se eventualmente surgir um novo perfil, ele só será incluído neste comportamento de obter diretórios quando uma nova declaração condicional for feita. Ficaria assim:

class ArquivoService{

    public String obterDiretorioPermitidoPorUsuario(Usuario usuario){
        
        if (usuario.perfilUsuario.nomePerfil == "aluno"){
            return "../arquivos-exclusivos/usuario-aluno/";
        }
         
        if (usuario.perfilUsuario.nomePerfil == "professor"){
            return "../arquivos-exclusivos/usuario-professor/";
        }

        if (usuario.perfilUsuario.nomePerfil == "coordenador"){
            return "../arquivos-exclusivos/usuario-coordenador/";
        }
    }

    
    {...} //outros metodos

}


Note como foi preciso alterar o código para que a funcionalidade pudesse abranger o novo perfil. Isso é sinal de que ele não está aberto para a extensão de seu comportamento sem que para isso haja alterações em sua implementação. Isso é exatamente o oposto do princípio Open/Closed.

Neste caso existem algumas possíveis maneiras de respeitar o princípio, uma delas ficaria da seguinte forma:

class ArquivoService{

    public String obterDiretorioPermitidoPorUsuario(Usuario usuario){
        
        return "../arquivos-exclusivos/usuario-"
                    + usuario.perfilUsuario.nomePerfil 
                    + "/";

    }

    {...} //outros metodos

}

Assim podem surgir infinitos novos perfis, o método irá abranger cada um deles sem que para isso seja necessário modificar uma linha sequer, até que a regra de negócio em si mude. Ou seja, enquanto a regra de negócio do método não mudar, o código permanecerá estático e funcional, mesmo com novos cenários surgindo ao longo do projeto.

Tendo essa característica, obterDiretorioPermitidoPorUsuario então é uma função que se encontra aberta para estender seu comportamento e fechada para modificações em seu código, obedecendo assim ao segundo princípio SOLID.

Passando pela definição do princípio e por exemplos de como violá-lo e respeitá-lo, é momento para desenvolver o discernimento de como escrever códigos complacentes. A seguir são explicadas algumas formas de pensamento que podem facilitar.

Máxima, Insumo e Resultado

Para o entendimento do princípio iremos construir um conceito utilizando estes três termos.

  • Máximas serão as regras gerais nas quais se baseiam as regras de negócio.
  • Insumos serão os diferentes valores de tipos bem definidos a serem consumidos pelas implementações das máximas.
  • Resultados serão os efeitos causados pela execução das máximas consumindo os insumos recebidos.

Tendo em mente a definição dos termos, ao ser presenteado com o pedido de um novo requisito no sistema, é importante saber identificar a regra máxima que rege o comportamento solicitado.

Para dar forma à ideia, o exemplo utilizado no início do artigo é destrinchado com o mapeamento de sua máxima:

  • Requisito: controle da obtenção de diretórios por perfil de usuário.
  • Máxima: com base no nome do perfil do usuário recebido (insumo de tipo bem definido), retornar um path personalizado para seu respectivo diretório (resultado do consumo do insumo).

A partir da máxima pode-se obter os possíveis comportamentos da feature requisitada:

  • caso o usuário seja do perfil aluno (insumo 1), o path será para a pasta de usuario-aluno (resultado 1);
  • caso o perfil seja professor (insumo 2), o path será para usuario-professor (resultado 2);
  • caso seja coordenador (insumo 3), será para usuario-coordenador (resultado 3);

O problema é que muitas vezes durante o entendimento da regra de negócio, sua máxima fica nas entrelinhas, se tornando implícita, fazendo com que o foco do desenvolvimento seja em cima de resultados específicos originados a partir dela, em vez de ser nela mesma.

É com esta inversão de foco que as possibilidades de comportamento se tornam hardcoded, em vez de serem geradas dinamicamente conforme o consumo de insumos, que foi exatamente o exemplo de como violar o Open/Closed no início do artigo: o código fonte do método em questão implementa os possíveis resultados, e não a máxima que os originam. Entender isso habilita a elaboração do conceito ao próximo tópico.

Programação Orientada à Máxima

O código fonte dos métodos deve ser um repositório de máximas, não de possíveis resultados. Na hora de codificar o sistema, são as máximas das regras de negócio que devem ser levadas em consideração, e não os seus possíveis desfechos, estes devem ser mera consequência causada pelos insumos consumidos. Em outras palavras: você deve codificar a máxima de um método, não os possíveis resultados que serão extraídos a partir dela.

Programando desta forma um método fica disponível para todas as possibilidades de comportamento de sua regra de negócio dinamicamente, ou seja, fica aberto para extensões. Isso anula a necessidade de ter codificada dentro de si cada situação possível à medida que elas vão surgindo, ou seja, ficando fechado para modificações; apenas sendo necessário alimentar os métodos com diferentes insumos de um mesmo tipo bem definido, para poder obter diferentes resultados em sua execução

Contrato de Insumo

Para que seja possível codificar orientado a máximas em vez de resultados, é importante mapear qual o tipo bem definido de insumos que serão consumidos na regra de negócio. No primeiro exemplo, a máxima sempre deverá interagir com insumos do tipo Usuario, que possuem um atributo do tipo Perfil, que por sua vez tem em si o campo nomePerfil.

Ter insumos de tipo bem definido faz com que a implementação da máxima possa se abstrair dos diferentes valores que serão consumidos, uma vez que, não importa quais forem, todos seguirão o mesmo contrato, por exemplo: possuir um campo do tipo Perfil com um atributo nomePerfil. 

Estas são premissas cruciais não só para o Open/Closed como também para o início do entendimento do conceito de inversão de dependência, cuja regra se baseia em fazer com que as funcionalidades (máximas implementadas) dependam de abstrações de tipos bem definidos (insumos injetados), e não de suas concretizações, o que também é relacionado ao conceito de hierarquia de abstrações, que foi explicado no artigo anterior de Single Responsibility. Mas este é um assunto para o artigo especial de Dependency Inversion, o último dentre os princípios SOLID.

Polimorfismo

Nem sempre escrever códigos complacentes com o Open/Closed será tão simples como foi no primeiro exemplo. Às vezes é necessário utilizar outros artifícios além de simplesmente reorganizar o código para remover algumas condicionais. — vale frisar que a violação do Open/Closed não é sobre ter condicionais implementadas, mas sim de inverter o foco do desenvolvimento para a codificação de possíveis resultados em vez de simplesmente codificar as suas máximas.

No exemplo que foi utilizado até este ponto, os insumos consumidos pela máxima da funcionalidade apenas englobavam em si valores de texto: "aluno", "professor", "coordenador" e assim por diante. O cenário nem sempre é este, o insumo a ser recebido também pode encapsular em si outras regras de negócio. Ou seja, em vez de trazer simplesmente valores para serem consumidos, eles trazem funcionalidades a serem injetadas de forma dinâmica.

É para tratar esse cenário que o polimorfismo (conceito de POO) se torna uma ferramenta bastante útil.

Quando se é respeitada a hierarquia de abstração, dentro da implementação dos métodos apenas encontramos níveis 1 em relação à funcionalidade proposta. Isso faz com que os métodos não estejam acoplados à codificação das regras de negócio que não são de sua responsabilidade, o que dá suporte ao princípio Open/Closed, pois o método fica acoplado às abstrações que são de seu interesse, mas como as abstrações encontram suas concretizações (como as funcionalidades de seu interesse são implementadas) não faz parte de sua jurisdição, o que permite a sua estabilidade de código e a possibilidade de flexibilidade em seus comportamentos.

É quando se organiza previamente o código seguindo estas regras, que podemos utilizar o polimorfismo ao nosso favor para garantir o cumprimento do Open/Closed.

Considere o seguinte exemplo:

class FormatacaoTextoService{

    //nivel 0 de abstracao
    public String[] formatarTextoPorRegrasEscolhidas(String texto,
                                                     RegraFormatacao[] regras){

        //nivel 1 de abstracao
        //cria array para armazenar o texto original e as suas formatacoes
        String[] textoFormatadoParaCadaRegra = [texto];

        //nivel 1 de abstracao
        //para cada regra de formatacao, adiciona no array seu resultado
        regras.paraCada(regra -> {

            textoFormatadoParaCadaRegra.add(regra.formatarTexto(texto));

        })

        //nivel 1 de abstracao
        //retorna o array
        return textoFormatadoParaCadaRegra;

    }

}

No exemplo acima o método formatarTextoPorRegrasEscolhidas recebe dois parâmetros: texto regras. A máxima de sua funcionalidade é, para cada regra de formatação que foi selecionada em algum passo anterior, passar o texto recebido para que este seja formatado.

Perceba que cada instância de regra de formatação possui em si um método formatarTexto, que deve ter suas respectivas regras de negócio específicas.

Para que formatarTextoPorRegrasEscolhidas possa receber quaisquer regras de formatação, o polimorfismo em cima da entidade RegraFormatacao pode ser utilizadoConfira isso nos próximos trechos de pseudocódigo.

Generalização RegraFormatacao:

class RegraFormatacao{

    public String formatarTexto(String texto){
        //sem implementacao
        return null;
    }
}


Especialização RegraFormatacaoSimples:

class RegraFormatacaoSimples extends RegraFormatacao{

    public String formatarTexto(String texto){

        texto = this.deixarInicialMaiuscula(texto);
        texto = this.deixarInicialAposPontosFinaisMaiuscula(texto);
        texto = this.colocarEspacoAposVirgulas(texto);
        return texto;

    }

    {...} //demais metodos

}


Especialização RegraFormatacaoAvancada:

class RegraFormatacaoAvancada extends RegraFormatacao{

    public String formatarTexto(String texto){

        texto = this.colocarPontuacoes(texto);
        texto = this.deixarInicialMaiuscula(texto);
        texto = this.deixarInicialAposPontosFinaisMaiuscula(texto); 
        texto = this.colocarEspacoAposVirgulas(texto); 
        texto = this.colocarAcentuacoes(texto);
        texto = this.criarParagrafosPorBlocosLogicos(texto);
        return texto;

    }

    {...} //demais metodos 

}


Ambas as especializações estendem a classe RegraFormatacao, cada uma implementando o método formatarTexto a sua própria maneira.

Este polimorfismo permite que o método formatarTextoPorRegrasEscolhidas possa receber tanto instâncias de RegraFormatacaoSimples como também de RegraFormatacaoAvancada, pois ambos são do tipo RegraFormatacao e implementam o método formatarTexto.

Podendo receber essas duas especializações e quaisquer outras que venham a surgir, o método se encontra aberto à possibilidade de executar quaisquer regras de formatação que existam no sistema, sem que para isso tenha sua implementação alterada.

Assim, mesmo que as seguintes classes sejam criadas posteriormente...

  • RegraFormatacaoComSeparadorUnderline
  • RegraFormatacaoTudoMaiusculo
  • RegraFormatacaoComSeparadorHifen
  • RegraFormatacaoComSepararAsteristico

...todas sendo especializações de RegraFormatacao, podem ser utilizadas e ter suas respectivas implementações injetadas de forma elegante no método formatarTextoPorRegrasEscolhidas, sem a dor da refatoração.

Desta forma tanto o princípio de responsabilidade única é respeitado, pois cada classe possui a responsabilidade de implementar uma única regra específica, como também o princípio de estar aberto para expansão de comportamentos e fechado para modificação de códigos é cumprido, afinal o método que é responsável por aplicar todas as regras selecionadas a um determinado texto está disponível para receber quaisquer regras que cumpram o contrato estabelecido.

Dentro de cada especialização por sua vez, as implementações do método formatarTexto também respeitam o princípio Open/Closed, pois cada uma é orientada à máxima de sua própria regra de negócio, tendo como a única razão para ser modificada, a mudança de sua regra máxima de negócio.

Este é o princípio Open/Closed em conjunto com o de Single Responsibility e algumas formas de pensamento para auxiliar a escrita de códigos complacentes.


0
79

Comentários (0)

Músico, Engenheiro de Software e apaixonado por criatividade.

Brasil