SOLID: Single Responsibility Principle (SRP) no Frontend

Um módulo deve ser responsável por um, e apenas um, ator - Robert Cecil Martin

SOLID: Single Responsibility Principle (SRP) no Frontend

No artigo anterior comentei sobre a Arquitetura Limpa no frontend. Mas no design limpo temos um conjunto de princípios chamado S.O.L.I.D. Dentro deste acrônimo temos o SRP (Single Responsability Principle), que segundo o próprio autor do livro pode causar confusão. Vamos trazer neste artigo pontos importantes direcionados também ao frontend e como precisamos tomar muito cuidado ao delegar responsabilidades para os componentes dentro do sistema ao escreve-los. Vamos iniciar com uma breve introdução do SRP.

Analogia

Podemos iniciar este artigo com uma analogia. Imagine que tenha um restaurante em sua cidade especializado em comidas típicas do país. Você ama esse lugar e todo mês está por lá.

Após alguns anos o estabelecimento está " sob nova direção! ". Isso te causa um pouco de preocupação e quando entra no local percebe que introduziram novos pratos ao cardápio, mas ainda trabalham com as comidas típicas do país local. Então pede ao garçom o seu prato preferido que o cozinheiro e sua equipe sabem preparar tão bem! Mas quando o prato chega você estranha a aparência da refeição. Tudo bem! O que é importante é o sabor... e ao dar a primeira garfada vem a decepção. A comida está com um sabor totalmente diferente do que era antes. Então você resolve falar com a gerencia do restaurante e pede para ir a cozinha.

Quando entra na cozinha entende o porque isso ocorreu. O chefe de cozinha e sua equipe ficaram sobrecarregados. Precisam atender dezenas de outros pratos novos com ingredientes diferentes. A especialidade deles era pratos típicos do país, mas agora preparam outros pratos tradicionais de outros países! Eles sabem como preparar pratos de outros países e também os locais. Mas por não lembrarem bem os ingredientes corretos para cada prato e por falta de tempo para preparem cada refeição, acabam confundindo esses ingredientes com os pratos típicos do país local. Isso gera insatisfação nos clientes e infelizmente não conseguem atender com excelência ninguém!

O problema é que aquela equipe era responsável por trabalhar com pratos específicos, eles tinham a responsabilidade de atender apenas clientes interessados nos pratos do país local. Mas agora também trabalhavam com pratos de outros países dos quais não são especialistas. Em vez de terem pessoas especializadas para preparar esses pratos, para atenderem os clientes interessados, toda a responsabilidade caiu para apenas uma equipe.

O princípio da responsabilidade única tem muito haver com isso. Principalmente se considerarmos a equipe da cozinha como classes, componentes ou módulos. Quem solicitou as mudanças foram os novos donos do estabelecimento (atores, departamento da empresa). O gerente pode ser considerado o desenvolvedor que não soube separar bem as responsabilidades com as mudanças de requisitos que ocorreram.

Mas se a analogia não ajudou muito fique tranquilo. Vamos abordar nos próximos tópicos exemplos em código. Vamos entender melhor este princípio.

Uma única responsabilidade?

Single Responsibility Principle, descrito no livro de Robert C. Martin pode levar qualquer desenvolvedor a pensar que um módulo, classe ou componente precisa ter apenas uma função que realiza apenas um propósito, levando para extremos que ao invés de ajudar podem atrapalhar.

Em certa ocasião tive a infelicidade de participar de uma reunião onde o gestor do projeto questionou um desenvolvedor do por que a classe X tinha três métodos declarados, com um tom de voz arrogante fez questão de explicar que havia uma clara violação do SRP pois a classe X teria que ter apenas um método conforme estava descrito no livro Clean Architecture. Obviamente aquele Gestor não leu com atenção o livro e muito menos entendeu sobre o que se trata o SRP!

O próprio autor do livro Arquitetura Limpa traça a linha do tempo deixando claro que antes tínhamos essa descrição para o SRP:

Um módulo deve ter uma, e apenas uma, razão para mudar

Obviamente que essas mudanças são solicitadas por alguém, que podemos chamar de usuários e stakeholders. O final da frase " razão para mudar " foi adaptada para esta versão:

Um módulo deve ser responsável por um, e apenas um, usuário ou stakeholder

Mas novamente não são as melhores palavras para serem usadas. Todo o software que você cria é, de uma forma ou de outra, feito para atender às necessidades de uma pessoa ou de um grupo de indivíduos. Quando o software muda, geralmente é para acomodar as mudanças nos requisitos desses atores. Com isso em mente podemos reformular o princípio como:

Um módulo deve ser responsável por um, e apenas um, ator

Os módulos são basicamente agrupamentos de funcionalidades que criam sequências lógicas de atividades, processamento e armazenamento de dados. O ator é quem pode solicitar a mudança daquele comportamento atual do aplicativo. Nas linguagens de programação mais populares podemos substituir módulos por classes.

Posso ter uma classe com mais de um ator? Não é recomendado fazer isso, pois seria violar o SRP, na verdade se você percebe que a sua classe está atendendo mais de um ator as chances no futuro de que isso afete financeiramente a empresa são altas! Além disso, pode ter inúmeros problemas com comportamentos inesperados devido ao alto acoplamento entre componentes. No livro Robert nos conta uma história que nos ajuda a visualizar como ter uma classe que cuida de mais de um ator pode prejudicar o desenvolvimento de futuras funcionalidades.

Também podemos ter problemas do lado do client caso nossos componentes, classes, funções ou módulos atendam mais de um grupo de usuários. Vamos entender melhor com exemplo em código!

SRP no Front-end

Vamos para um exemplo simples mas que pode ajudar a entender melhor como no frontend precisamos ter cuidado e analisar bem a separação das responsabilidades entre componentes.

Veja o método abaixo:

    public consultarCep(cep: string): Observable<CepConsulta> {
        return this.http
            .get<CepConsulta>(`https://viacep.com.br/ws/${cep}/json/`)
            .pipe(catchError(super.serviceError))
    }

Quando olhamos esse método e analisamos seu nome vemos que se trata de uma consulta de endereço. Estamos utilizando uma API de um terceiro para realizar uma consulta Via Cep. Mas veja o nome da classe (no caso uma service) que essa função está declarada.

export class FornecedorService

FornecedorService? O que isso tem relação com a consulta de endereço? Podemos entender que todo fornecedor precisa ter um endereço físico. Mas não concorda que ocorreu uma decisão precipitada ao colocar esse método dentro de um serviço que tem como responsabilidade cuidar de assuntos que são exclusivos do fornecedor? Podemos dizer que essa classe cuida apenas de um ator? No caso não podemos! A classe tem mais de uma responsabilidade! Ele sabe como consultar um endereço utilizando o ViaCep. Agora imagine que outro desenvolvedor está responsável em criar o formulário de cadastro do cliente que vai poder comprar de algum fornecedor um produto X. O que foi definido pela equipe de negócios é que o cliente vai ter a opção de inserir seu CEP para realizar o cadastro para efetivação da compra. Então o programador não tendo conhecimento daquele método, cria uma classe e suas dependências para User e uma nova implementação utilizando a API do ViaCep. No final temos violações dos princípios do SOLID e além disso também do DRY repetindo conhecimento em nossa aplicação!

Qual seria a solução?

Criar uma classe (em outro contexto poderia ser um módulo ou componente) que tem exatamente essa responsabilidade. Assim isolamos a classe fornecedor de ser afetado se as implementações do ViaCep forem alteradas no futuro. Assim deixamos essa classe cuidar apenas das responsabilidades que tem relação com um fornecedor e evitamos duplicar conhecimento no restante da aplicação.

@Injectable()
export class ViaCEPService {
    constructor(private readonly httpClient: HttpClient) { }

    public consultarCep(viaCep: string): Observable<CepConsulta> {
          return this.http
            .get<CepConsulta>(`https://viacep.com.br/ws/${cep}/json/`)
            .pipe(catchError(super.serviceError))
        );
    }
}

Olhando para componentes

Os nossos componentes são importantes, mas devemos ter cautela e pensar bem em como separar bem as responsabilidades. Veja esse HTML, que faz parte do cadastro de um novo fornecedor:

 <h4>Endereço</h4>
        <hr>
        <div formGroupName="endereco">
          <div class="form-group">
            <label class="control-label">CEP</label>
            <div>
              <input class="form-control" id="cep" type="text" placeholder="CEP (requerido)"
                (blur)="buscarCep($event.target.value)" formControlName="cep" cep
                [textMask]="{mask: MASKS.cep.textMask}" [ngClass]="{ 'is-invalid': displayMessage.cep }" />
              <span class="text-danger" *ngIf="displayMessage.cep">
                <p [innerHTML]="displayMessage.cep"></p>
              </span>
            </div>
          </div>
          <div class="form-group">
            <label class="control-label">Logradouro</label>
            <div>
              <input class="form-control" id="logradouro" type="text" placeholder="Logradouro (requerido)"
                formControlName="logradouro" [ngClass]="{ 'is-invalid': displayMessage.logradouro }" />
              <span class="text-danger" *ngIf="displayMessage.logradouro">
                <p [innerHTML]="displayMessage.logradouro"></p>
              </span>
            </div>
          </div>
          <div class="form-group">
            <label class="control-label">Número</label>
            <div>
              <input class="form-control" id="numero" type="text" placeholder="Número (requerido)"
                formControlName="numero" [ngClass]="{ 'is-invalid': displayMessage.numero }" />
              <span class="text-danger" *ngIf="displayMessage.numero">
                <p [innerHTML]="displayMessage.numero"></p>
              </span>
            </div>
          </div>
          <div class="form-group">
            <label class="control-label">Complemento</label>
            <div>
              <input class="form-control" id="complemento" type="text" placeholder="Complemento"
                formControlName="complemento" [ngClass]="{ 'is-invalid': displayMessage.complemento }" />
              <span class="text-danger" *ngIf="displayMessage.complemento">
                <p [innerHTML]="displayMessage.complemento"></p>
              </span>
            </div>
          </div>
          <div class="form-group">
            <label class="control-label">Bairro</label>
            <div>
              <input class="form-control" id="bairro" type="text" placeholder="Bairro (requerido)"
                formControlName="bairro" [ngClass]="{ 'is-invalid': displayMessage.bairro }" />
              <span class="text-danger" *ngIf="displayMessage.bairro">
                <p [innerHTML]="displayMessage.bairro"></p>
              </span>
            </div>
          </div>
          <div class="form-group">
            <label class="control-label">Cidade</label>
            <div>
              <input class="form-control" id="cidade" type="text" placeholder="Cidade (requerido)"
                formControlName="cidade" [ngClass]="{ 'is-invalid': displayMessage.cidade }" />
              <span class="text-danger" *ngIf="displayMessage.cidade">
                <p [innerHTML]="displayMessage.cidade"></p>
              </span>
            </div>
          </div>
          <div class="form-group">
            <label class="control-label">Estado</label>
            <div>
              <select class="form-control" id="estado" formControlName="estado"
                [ngClass]="{ 'is-invalid': displayMessage.estado }">
                <option value="">Estado</option>
                <option value="AC">Acre</option>
                <option value="AL">Alagoas</option>
                <option value="AP">Amapá</option>
                <option value="AM">Amazonas</option>
                <option value="BA">Bahia</option>
                <option value="CE">Ceará</option>
                <option value="DF">Distrito Federal</option>
                <option value="ES">Espírito Santo</option>
                <option value="GO">Goiás</option>
                <option value="MA">Maranhão</option>
                <option value="MT">Mato Grosso</option>
                <option value="MS">Mato Grosso do Sul</option>
                <option value="MG">Minas Gerais</option>
                <option value="PA">Pará</option>
                <option value="PB">Paraíba</option>
                <option value="PR">Paraná</option>
                <option value="PE">Pernambuco</option>
                <option value="PI">Piauí</option>
                <option value="RJ">Rio de Janeiro</option>
                <option value="RN">Rio Grande do Norte</option>
                <option value="RS">Rio Grande do Sul</option>
                <option value="RO">Rondônia</option>
                <option value="RR">Roraima</option>
                <option value="SC">Santa Catarina</option>
                <option value="SP">São Paulo</option>
                <option value="SE">Sergipe</option>
                <option value="TO">Tocantins</option>
              </select>
              <span class="text-danger" *ngIf="displayMessage.estado">
                <p [innerHTML]="displayMessage.estado"></p>
              </span>
            </div>
          </div>
        </div>

É evidente que se trata de um HTML que cuida apenas de atributos que dizem a respeito de um endereço. Mas por que esse HTML está declarado dentro do contexto de fornecedor? Além disso veja quantos inputs temos! Agora imagine uma página completa de cadastro de fornecedor. O HTML poderia facilmente ser extenso e confuso de se ler dificultando a manutenção. Mas o pior está por vir. E se eu te dizer que para cada componente, de cadastro e edição, esse HTML se repete sempre? É assustador lidar com componentes que tem múltiplas responsabilidades. Caso outro programador precise utilizar em outra parte da aplicação o objeto endereço, teria que replicar o mesmo HTML? Acaba se tornando um loop sem fim!

A solução para este caso seria criar um componente que tem como responsabilidade o endereço. Esse componente pode ser reutilizado quando for necessário em outras partes do sistema. Assim seria apenas necessário chamar o seletor desse componente no HTML.

Veja outro exemplo:

onSubmit() {
        let userlogin = this.insertForm.value;
        this.acct
            .login(userlogin.Username, userlogin.Password, userlogin.RememberMe)
            .subscribe(
                (result) => {
                    this.invalidLogin = false;
                    $('body').css({ 'background-image': 'none' });
                    this.router.navigateByUrl(this.returnUrl);
                },
                (error) => {
                    this.invalidLogin = true;
                    this.ErrorMessage = error;
                    if (error.status == 500) {
                        this.toasterService.info(
                            'Our Team is working to fix this error. Try again later.',
                            '',
                            { positionClass: 'toast-top-right' }
                        );
                    }
                    if (error.status == 401) {
                        if (error.error.loginError) {
                            if (
                                error.error.loginError == 'Auth Code Required'
                            ) {
                                localStorage.setItem(
                                    'codeExpiry',
                                    error.error.expiry
                                );
                                localStorage.setItem(
                                    'twoFactorToken',
                                    error.error.twoFactorToken
                                );
                                localStorage.setItem('isSessionActive', '1');
                                localStorage.setItem(
                                    'user_id',
                                    error.error.userId
                                );
                                this.router.navigate(['/send-code']);
                                return false;
                            }
                            if (error.error.loginError == 'Account Locked') {
                                Swal.fire({
                                    title:
                                        'Your account is locked, please contact support.',
                                    icon: 'error',
                                    showClass: {
                                        popup:
                                            'animate__animated animate__fadeInDown'
                                    },
                                    hideClass: {
                                        popup:
                                            'animate__animated animate__fadeOutUp'
                                    }
                                });
                                return false;
                            }
                            if (
                                error.error.loginError == 'Email Not Confirmed'
                            ) {
                                Swal.fire({
                                    title:
                                        'Your Email is not Verified. Please Verify Email',
                                    icon: 'error',
                                    showClass: {
                                        popup:
                                            'animate__animated animate__fadeInDown'
                                    },
                                    hideClass: {
                                        popup:
                                            'animate__animated animate__fadeOutUp'
                                    }
                                });
                                return false;
                            } else {
                                this.toasterService.error(
                                    error.error.loginError,
                                    '',
                                    { positionClass: 'toast-top-right' }
                                );
                                return false;
                            }
                        } else {
                            this.toasterService.error(
                                'Invalid Username/Password. Please check credentials and try again',
                                '',
                                {
                                    positionClass: 'toast-top-right',
                                    timeOut: 3000
                                }
                            );
                            return false;
                        }
                    } else {
                        this.toasterService.warning(
                            'An error occured while processing this request.',
                            '',
                            { positionClass: 'toast-top-right' }
                        );
                        return false;
                    }
                }
            );
    }

O código acima faz parte de um componente que deveria apenas enviar dados referente a um login. Claramente vemos que o método onSubmit é muito poluído! Veja como existe uma cascata de if/else que prejudicam a leitura. É possível observar que a função é responsável por manipular o localStorage. Além de tudo isso seta CSS $('body').css({ 'background-image': 'none' });.

A solução para este caso é primeiro entender toda a abordagem da função e iniciar uma refatoração nela utilizando as recomendações de Martin Fowler abordadas no livro Refactoring: Improving the Design of Existing Code. Veja um trecho bastante interessante onde ele comenta exatamente sobre isso:

Em 99% do tempo, tudo que você tem a fazer para reduzir uma função é usar a técnica extrair função (extract function). Encontre partes da função que pareçam ficar bem se estiverem juntas e crie uma nova função.

Então olhando para este código sujo e que viola o SRP, o desenvolvedor poderia separar as repetições de condicionais que existem isolando em uma classe ou função que seria responsável por verificar se o erro é do tipo 401, 403, 404, 500 e outros conforme for necessário:

import { HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';

@Injectable({
  providedIn: 'root'
})
export class ErrorHandlerService {
  public errorMessage: string = '';

  constructor(private router: Router) { }

  public handleError = (error: HttpErrorResponse) => {
    if (error.status === 500) {
      this.handle500Error(error);
    }
    else if (error.status === 404) {
      this.handle404Error(error)
    }
    else {
      this.handleOtherError(error);
    }
  }

  private handle500Error = (error: HttpErrorResponse) => {
    this.createErrorMessage(error);
    this.router.navigate(['/500']);
  }

  private handle404Error = (error: HttpErrorResponse) => {
    this.createErrorMessage(error);
    this.router.navigate(['/404']);
  }
  private handleOtherError = (error: HttpErrorResponse) => {
    this.createErrorMessage(error);
  }

  private createErrorMessage = (error: HttpErrorResponse) => {
    this.errorMessage = error.error ? error.error : error.statusText;
  }
}

O código acima é apenas um exemplo de como podemos passo a passo refatorar e pensar melhor nas responsabilidades de cada função em nosso frontend. Existem melhores práticas para lançamentos de erros dentro do front e podemos abordar isso em outro artigo. Outro ponto a destacar é que o localStorage poderia ficar em uma classe separada, assim em caso de outro componente da aplicação precisar utilizar o localStorage.setItem bastaria realizar a importação da classe e injetar no construtor. Novamente isso é apenas uma sugestão de como realizar.

Conclusão

Seguir boas práticas pode te ajudar muito no dia a dia! O SRP é valioso demais para não ser levado a sério. Entender como separar as responsabilidades de classes e componentes dentro do sistema não é uma tarefa simples principalmente quando estamos trabalhando para corporações grandes do mercado! Por isso é importante continuar estudando e aprimorando a cada dia. Como desenvolvedores é nosso dever defender códigos legíveis e arquiteturas limpas. Lembre-se que definir bem as responsabilidades de classes, componentes e camadas pode garantir o fracasso ou sucesso do software.

Fico por aqui neste artigo e espero que de alguma forma este conteúdo possa ter auxiliado para o entendimento do princípio da responsabilidade única. Até o próximo!

Referência:

Blog Robert Martin

Blog Martin Fowler