Executando verificação de segurança...
2

A importância dos princípios da programação (pela perspectiva de um iniciante).

Iniciando um pensamento...

A ideia desse post surgiu de algumas experiências que tive recentemente ao longo dos meus estudos. Para contextualizar um pouco, deixe eu falar um pouco sobre mim. Atualmente estudo programação a quase um ano (11 meses sendo exato) e venho de outra área (advogado), mas esse não é o ponto, o ponto central é que desde que comecei meus estudos tinha o desejo de construir uma app assistente do jogo League of Legends. Após ter aprendido JS/TS e React resolvi que era hora de por a mão na massa.

Pesquisei como fazer uma aplicação desktop com JS e descobri o electron, resolvi usar electron + react, instalei todas as bibliotecas necessárias e segui. Surpreendentemente o projeto foi andando e hoje já está perto de ser finalizado (quem tiver curiosidade deixei o link do projeto no github como fonte).

Esse projeto me ajudou a aprender bastante coisa e a polir meus conhecimentos (na realidade eu queria usar ele para estudar React, o que foi um exito de forma geral). Agora preciso introduzir outro personagem na história, meu irmão. Meu irmão é um programador experiente, tem quase uns 20 anos de carreira, ele tem me ajudado ao longo desse percurso. No início da semana passada conversei com ele sobre estar um pouco perdido nos estudos, não saber exatamente o que deveria estudar, e ele me recomendou testar minha aplicação. Achei a proposta interessante e segui para fazer testes.

O início de uma saga (que ainda não foi concluída).

Abri meu projeto no VSCode, configurei o Jest, e comecei a ler a documentação. Comecei testando meus componentes React, tive alguns percalços, mas aos trancos e barrancos fui conseguindo testar e dar 100% de coverage nos components.

A dor de cabeça de verdade começou quando eu precisei testar a seguinte função que eu tinha escrito:

function LeagueOfLegendsClientInstance() {
    const lockfile = JSON.parse(localStorage.getItem('LockfileData') as string) as ILockfileData;
    return axios.create({
      baseURL: `https://127.0.0.1:${lockfile.port}`,
      auth: {
        username: 'riot',
        password: `${lockfile.password}`,
      },
    });
  }
}

Quando escrevi essa função me parecia sem nenhum problema, era simples, pega um objeto no local storage e retornava uma instância do axios utilizando as propriedades daquela objeto. Grande engano, na hora de testar percebi que o JEST não acessava o local storage, mockar o local storage me parecia uma tarefa hercúlea também, passei horas tentando solucionar o problema, até que olhei para a função novamente e bateu um estalo na minha cabeça: "Essa função está fazendo mais do que deveria". O objetivo dela era simplesmente retornar uma instância configurada do axios, não fazia sentido a função acessar o local storage, faria mais sentido ela receber o objeto 'lockfile' pronto. Eis que essa função se torna a seguinte classe:

export default class LeagueOfLegendsClient {
  constructor(public lockfile: ILockfileData) {}

  LeagueOfLegendsClientInstance() {
    return axios.create({
      baseURL: `https://127.0.0.1:${this.lockfile.port}`,
      auth: {
        username: 'riot',
        password: `${this.lockfile.password}`,
      },
    });
  }
}

Ter esse insight faz eu abrir um sorriso de canto a canto da boca até o presente momento, algo que era muito complexo de testar agora me parecia muito simples. Abri meu arquivo de testes, rodei os testes e estava feito, a função tinha sido testada com 100% de coverage. O código do teste foi o seguinte:

describe('LeagueOfLegendsClient Lib Tests', () => {
  it('"LeagueOfLegendsClientInstance" should be return an axios instance', () => {
    const lolClient = new LeagueOfLegendsClient({ port: '8876', password: '123456' });
    const lolAxiosInstance = lolClient.LeagueOfLegendsClientInstance();
    const spy = jest.spyOn(lolAxiosInstance, 'get');
    lolAxiosInstance.get('');

    expect(spy).toBeCalled();
  });
});

Falando sobre fundamentos.

Agora podemos retornar ao assunto principal do post. Quando estamos começando nos estudos é difícil perceber o motivo de alguns pensamentos terem sido desenvolvidos, esse pequeno exemplo serviu para abrir meus olhos sobre dois pensamentos fundamentais que eu vinha lendo sobre a algum tempo, mas nunca tinha entendido exatamente a sua importância: 'injeção de dependências' e 'funções devem fazer apenas uma coisa'.

Veja, eu passei horas tentando resolver um problema que teria sido facilmente resolvido se eu tivesse prestado real atenção na hora de escrever meu código. A partir dessa dor de cabeça pude perceber olhando para outras partes do meu código onde eu teria problema para testar e onde eu iria precisar refatorar, isso aconteceu na sexta-feira passado, dia 14. Hoje, três dias depois, refatorei uma parte do meu código e notei que pelo menos uns 30% dele não era testável de forma eficiente, até mesmo consegui perceber onde eu poderia aplicar alguns padrões de projetos que facilitariam ainda mais minha vida.

O que podemos levar de aprendizado?

É bem verdade que quando começamos no mundo da programação somos afogados num mar exuberante de informações. São vários vídeos, livros, linguagens, tecnologias e por aí vai. Vamos loucamente nos afundado nesse mar e aprendendo na prática muitas dessas tecnologias e esquecemos, ou até mesmo não queremos, de buscar compreender coisas mais fundamentais como orientação a objetos, padrões de projetos, princípios SOLID, entre outros. Sei disso, pois fui um desses que resistiu bastante a tentar entender o que esses conceitos significavam e mesmo quando fui atrás tive uma dificuldade enorme de entender o motivo de sua existência.

Esse texto tem apenas dois objetivos: o primeiro é relatar minha experiência recente e que abriu meus olhos para algumas coisas, o segundo é tentar passar a visão, de iniciante para iniciante, de que esses temas que sempre observamos os outros falarem não são firulas, pelo contrário, tem uma aplicação prática muito útil e podem facilitar nossas vidas. Como eu disse, passei horas (facilmente mais de três) tentando testar uma função que seria muito mais facilmente testável se ela fosse escrita de outra forma.

Eu poderia passar mais algumas linhas de texto relatando como tive insights parecidos com essa ao longo desse fim de semana ou até mesmo relatando outros problemas que tive enquanto testava o código do meu projeto, mas creio que esse pequeno exemplo já sirva como ilustração do post. Desta forma, deixarei o meu lado prolixo de advogado de lado dessa vez.

Tudo isto posto, espero que esse texto seja interessante de se ler para aqueles que já são mais experientes, e para estes também peço que contribuam com mais informações sobre o assunto. E para aqueles que estejam começando, assim como eu, que esse texto possa acender uma lampadazinha na sua mente e te faça olhar novamente o seu código daquele projetinho lá.

Carregando publicação patrocinada...
2

Sobre "funções devem fazer apenas uma coisa", vale lembrar que "uma coisa" é bem subjetivo e varia bastante conforme o contexto.

Por exemplo, "fechar pedido" é "apenas uma coisa"? Ou é algo que pode ser decomposto em partes como "verificar disponibilidade no estoque", "fazer pagamento", "gerar nota fiscal", etc? Mas cada uma dessas partes, também não pode ser decomposta?

De qualquer forma, o que impede de ter uma função que fecha o pedido, que por sua vez chama as demais? Algo como:

function fecharPedido() {
    verificarEstoque();
    pagamento();
    gerarNF();
}

É uma função, mas ela ainda faz "apenas uma coisa"? Do ponto de vista negocial, pode ser que "fechar pedido" configure uma "ação única" (ou não, sei lá).

E cada uma das funções pode ser quebrada em outras menores, e assim por diante. Mas onde parar exatamente? Não dá pra saber, não é uma ciência exata. De forma geral, vc deve parar quando achar que não faz mais sentido quebrar em partes menores. Medir a "testabilidade" é um dos critérios válidos, por exemplo. Mas continua tendo um componente subjetivo.

Tem gente que gosta de cravar números mágicos ("uma função não pode ter mais que X linhas"), o que eu acho meio besta. Nenhum número arbitrário é a verdade absoluta, mas em geral todos concordam que funções muito grandes são difíceis de ler, testar e dar manutenção. Mas quanto é "muito grande"? Depende, não tem número mágico, e qualquer um que tente definir um valor qualquer estará focando no problema errado. Avaliar o contexto é mais importante do que seguir cegamente uma regra (ainda mais uma tão arbitrária e falha quanto o número máximo de linhas).

Enfim, o que vale é a regra geral: se a função começar a ficar muito grande ou focar em mais de uma coisa, é um sinal de que vc pelo menos deveria parar e pensar sobre ela, ver se não vale a pena quebrar em mais funções, ou se tudo bem porque no fundo tudo aquilo que ela faz na verdade é uma coisa só (e aí seria o caso de renomear a função). Senso crítico sobre o próprio código costuma ser melhor do que seguir regrinhas.

1

Concordo em número, gênero e grau. Esse tipo de regra é mais um norte pra buscar melhorar a qualidade do codigo do que realmente uma lei absoluta.

No meu caso a testabilidade fez eu pensar um pouco melhor em como eu poderia escrever meu código.