0

Desbravando princípios SOLID: Single Responsibility

José Júnior
José Júnior

Quem tem experiência com desenvolvimento de software sabe como pode se tornar um desprazer dar manutenção em certos tipos de código e garantir a implementação de novas features sem quebrar o que está funcionando. Manutenção e escalabilidade são tópicos delicados, ambos cruciais para longevidade saudável dos projetos.

Para que seja possível garantir uma boa relação com esses dois pontos vitais do ciclo de desenvolvimento de software, existem cinco princípios que, juntos, dão origem ao acrônimo SOLID. Cada princípio traz regras de boa prática para serem seguidas no momento da codificação.

São eles os seguintes princípios: Single ResponsibilityOpen-ClosedLiskov's SubstitutionInterface Segregation e Dependency Inversion.

Para dar início ao desbravamento, será respeitada a ordem das letras do acrônimo para a postagem de cada artigo, começando assim pelo primeiro princípio: Single Responsibility, ou Responsabilidade Única.

Single Responsibility (Responsabilidade Única)

Uma classe deve possuir uma, e apenas uma, responsabilidade.

O princípio é sobre, para cada atividade central que um programa terá, criar uma classe cuja responsabilidade é implementar suas funcionalidades.

Vejamos um exemplo de como violar este princípio com um pouco de pseudocódigo:

class CompraService{

    @Autoinstanciavel
	private BaseDado baseDado;

    @Autoinstanciavel
	private RestService restService;

    //metodo para efetuacao de compras
	public void efetuarCompra(Compra compra){
        
        //Setta a data da efetuacao para o momento em que o codigo eh executado
        compra.setDataCompraEfetuada(DataUtil.dataAtual);

		
        //Inicia conexao com base
        baseDado.iniciarConexao();

        //Gera comando de insert para compras 
	    String insertCompra = baseDado.gerarComandoInsert(compra);

		//Salva na base a compra
        baseDado.executarComando(insertCompra);

        //Cria instancia de email
        EmailCompraEfetuada email = new EmailCompraEfetuadal();

		//Passa a compra p corpo do email
        email.setCompra(compra);

        //Manda o email p API de enviar emails
		restService.post(“api/emails”, email);
    }

}


No trecho acima temos 1 classe com 3 responsabilidades: implementação do gerenciamento de compras, de base de dados e da criação de e-mails.

Isso fere o princípio de responsabilidade única, e como consequência, por mais simples que pareça no exemplo acima, a manutenção desse código se torna mais difícil, o deixando suscetível a bugs. Isso se dá por dois principais motivos:

  1. implementação da classe está maior do que deveria, comprometendo sua legibilidade, que deveria ser mais simples.
  2. por ter mais de um tipo de atividade sendo implementada no mesmo lugar, caso uma delas precise ser alterada, o código que será mexido é o da implementação do método no qual as três atividades estão implementadas, causando o risco da modificação influenciar na funcionalidade de todas, o que poderia ser evitado se o princípio de responsabilidade única tivesse sido respeitado.

Para resolver isso basta seguir a regra do princípio: cada classe deve ter uma única responsabilidade!

Após a refatoração, CompraService ficaria da seguinte forma:

class CompraService{

    @Autoinstanciavel
	private CompraRepository compraRepository;

    @Autoinstanciavel
	private EmailCompraEfetuadaService emailCompraEfetuadaService;

    //Metodo para efetuacao de compras
	public void efetuarCompra(Compra compra){
       
           //Setta a data de efetuacao
           compra.setDataCompraEfetuada(Data.dataAtual);

           //Manda salvar na base
           compraRepository.salvarCompraEfetuada(compra);
 
           //Manda enviar email
           emailCompraEfetuadaService.enviarEmailCompraEfetuada(compra);
    }

}

Agora CompraService possui a única responsabilidade de gerenciar compras efetuadas, passando a data do momento da efetuação para a compra e em seguida passando a compra para as outras classes responsáveis por executar suas respectivas responsabilidades: salvar a compra na base (CompraRepository) e enviar um e-mail de efetuação de compra (EmailCompraEfetuadaService).

Desta forma até mesmo os comentários no código são dispensáveis uma vez que a chamada dos métodos por si só já é autoexplicativa sobre o que está acontecendo na implementação do método efetuarCompra(), bastando apenas ler o que está escrito.

Caso haja dúvidas sobre aonde aquele código todo foi parar, simplesmente cada implementação foi para dentro de um método de sua respectiva classe:

Classe CompraRepository

class CompraRepository{

    @Autoinstanciavel
	private BaseDado baseDado;

	//Metodo para salvar compras efetuadas
    public Compra salvarCompraEfetuada(Compra compra){

        //Inicia conexao c base de dados
        baseDado.iniciarConexao();

		//Manda gerar o comando p insert
        String insertCompra = baseDado.gerarComandoInsert(compra);

		//Manda salvar executar comando p salvar
        baseDado.executarComando(insertCompra);

    }

}

Classe EmailCompraEfetuadaService

class EmailCompraEfetuadaService{

    @Autoinstanciavel
	private RestService restService;

    //Metodo de enviar email com compra efetuada
	public Compra enviarEmailCompraEfetuada(Compra compra){

        //Cria instancia de email
        EmailCompraEfetuada email = new EmailCompraEfetuadal();

		//Configura o email passando a compra
        email.setCompra(compra);

        //Manda a instancia de email p API de enviar emails
		restService.post(“api/emails”, email);

    }


}

Tendo para cada classe uma responsabilidade, garantimos o cumprimento deste primeiro princípio.

Entendendo os cenários de definição da violação e cumprimento da responsabilidade única, chega o momento de adquirir o discernimento de como escrever classes complacentes. Para isso algumas formas de pensamento podem auxiliar. Acompanhe quais são abaixo.

Funcionalidade vs. Implementação

Ter noção desses dois termos irá ajudar nos próximos passos, habilitando o entendimento do conceito de níveis de abstração. Funcionalidade é o que é feito, com alto nível de abstração em relação à codificação; implementação é como a funcionalidade foi construída em nível de código, ou seja, com baixo nível de abstração em relação à codificação. — Abstração se trata da disposição de ignorar pontos irrelevantes para um determinado objetivo.

Por exemplo, a funcionalidade de enviar e-mails para clientes que estão há meses sem comprar nada, seria representada pela chamada do método enviarEmailParaClientesInativos(), dispensando preocupações sobre como a função está codificada, ou seja, tendo alto nível de abstração em relação à codificação. Já a sua implementação é o que determina a lógica por de baixo dos panos para que seja possível executá-la, sendo assim considerada de baixo nível de abstração.

Todos os comportamentos implementados em uma classe devem estar dentro do escopo de sua atividade central

Por exemplo, se a classe tem como função gerenciar compras, enviar e-mails pode até ser um comportamento relacionado no sentido de que pode ser disparado por uma compra sendo realizada ou cancelada, mas sua implementação não está dentro do escopo de gerenciar compras, e sim de enviar e-mails. Por isso uma nova classe deve ser criada para comportar a implementação de enviar e-mails, desta forma a classe de gerenciar compras pode chamar a funcionalidade de enviar e-mails quando bem entender, mas abstraindo-se de como a funcionalidade de enviar e-mails é codificada. Em outras palavras, na classe de gerenciar compras pode até ser interessante ter injetada a funcionalidade de enviar e-mails, mas como a funcionalidade é implementada não é de sua responsabilidade, isso (a implementação) fica por conta de uma outra classe especializada: a classe de enviar e-mails.

Hierarquia de abstração

Classes devem ser claras enquanto ao que podem fazer a partir do nome de seus métodos, e todos os métodos devem estar dentro do escopo de sua atividade central. A implementação de seus métodos por sua vez deve ser apenas 1 nível abaixo na camada de abstração em relação a sua funcionalidade.

Imagem ilustrando inception

A hierarquia de abstração é quase que recursiva, assemelhando-se ao termo popularmente conhecido como inception, onde aquilo que é a implementação dentro de uma funcionalidade, também é uma funcionalidade com suas implementações, que essas por sua vez têm dentro de si outras funcionalidades com implementações próprias e assim vai, até o último nível de abstração de um escopo. Para evitar confusão é preciso estabelecer bem qual o escopo no qual está se codificando.

Exemplificando: é solicitada a implementação de um método para salvar contatos. A funcionalidade em si de salvar contatos representa o nível 0 da hierarquia de abstração neste escopo; os passos necessários para implementar a funcionalidade (mandar salvar na base de dados e gravar num arquivo de log o que foi feito) representam o nível 1; A forma como esses passos são implementados é representada pelo nível 2, e assim por diante.

Destrinchando melhor o escopo exemplificado, esses são os passos para implementar a funcionalidade de salvar contatos:

  • Salvar a instância de Contato na base de dados (primeiro passo, nível 1 de abstração em relação à funcionalidade. Como fazer para salvar uma instância na base representa o nível 2 em relação à funcionalidade objetivo)
  • Gravar no arquivo de log a execução feita (segundo passo, nível 1 de abstração em relação à funcionalidade. Como fazer para gravar em arquivos de log representa o nível 2 em relação à funcionalidade objetivo)

Vejamos como ficaria a implementação de forma errônea:

class ContatoService{
      
      
      // nível 0
      public void salvarContato(Contato contato){ 
      
             // nivel 1 
             contatoRepository.salvarContato(contato);
      
             // nivel 2 
             Arquivo arquivo = ArquivoUtils.abrirArquivo("./log");
      
             // nivel 2
             arquivo.escrever("Contato salvo às " + DataUtil.dataAtual);
      
             // nivel 2
             arquivo.concluirEscrita();
        
        }
}
      

A primeira linha da implementação do método é sobre claramente o que é preciso para implementar o método, já da linha posterior em diante é sobre como codificar o que é preciso, para só então poder implementar o método. Desta forma há uma mistura de níveis de abstração dentro de uma mesma função, o que indica uma má escrita de código.

Para respeitar o princípio de responsabilidade única, dentro do método seria necessário que fossem presentes exclusivamente os níveis 1 de abstração em relação à funcionalidade proposta.

Isso faz com que seja necessário criar um novo método responsável por gravar arquivos de log, para que este seja uma funcionalidade a ser chamada na implementação do método de salvar contatos. É então quando deve-se retomar o pensamento de que todos os comportamentos implementados em uma classe devem estar dentro do escopo de sua atividade central.

Um método que grava em arquivos de log está relacionado à atividade central de gerenciar contatos? Não!

Sendo assim, se faz mandatória a criação de uma nova classe para implementar a funcionalidade de gravar em arquivos de log, respeitando o princípio de responsabilidade única em escopo de classes. Caso contrário, bastaria criar um novo método dentro da mesma classe.

Refatorando o pseudocódigo seguindo o princípio, ficaria assim:

class ContatoService{
      
      
      @Autoinstanciavel
      LogService logService;
      
      // nível 0
      public void salvarContato(Contato contato){ 
      
             // nivel 1 
             contatoRepository.salvarContato(contato);
    
      
             // nivel 1
             logService.escreverLog("Contato salvo");       

      }

}

E na nova classe especializada em Logs:

class LogService{

      //nivel 0
      public void escreverLog(String texto){
               
              //nivel 1
              Arquivo arquivo = ArquivoUtils.abrirArquivo("./log");
              
              //nivel 1
              arquivo.escrever("["+DataUtil.dataAtual+"] : " + texto);

              //nivel 1  
              arquivo.concluirEscrita(); 
      }
}


Este foi um desbravamento dos fundamentos para o cumprimento do princípio de Single Responsibility do padrão SOLID com pitadas de Clean Code.


0
45

Comentários (0)

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

Brasil