DRY

É isso, eu gosto de começar os meus textos com clickbaits, porém não vai ser o caso desse texto. Deixar eu entrar logos nos detalhes e fazer algo que é impossível no Twitter, dar o contexto correto, por falta dele fui até chamado como membro da igreja do DRY, talvez eu seja um pouco, porém...

Então vamos para o contexto sobre o quando e porque estou escrevendo esse texto. já são 03:30 da madrugada e estou esperando o fiasco da F1 completar reparos e dizer se vai ou não acontecer os treinos livres 2 no novo circuito de Las Vegas.

Detalhe amanhã sexta-feira e será daqueles finais de semana que bagunço ainda mais minha rotina já bagunçada de sono só por conta de carros correndo e meu Twitter tem sido um caos nesse momento, mesmo de madrugada, então aproveito para escrever esse texto e consegui explicar algumas coisas.

O tweet em questão é esse

Vou tentar detalhar cada ponto e dar mais contexto, começando pela estrutra e o que realmente é o malfadado código. Antes é importante destacar que estamos falando de NodeJS, alguns conceitos podem ser aplicados a outros contexto, mas aqui é NodeJS com Express e sem Typescript. (Como assim sem Typescript?! Explico mais aqui)

Isso é um adapter, que faz transformações entre use-cases e um services da API do Fiança Rápida. Então detalhando melhor nossa estrutura, toda a regra de negócio é isolada de agentes externos, coisas como models/repositories, comunicação externa, controllers e schemas ficam de fora das use-cases.

E quase tudo dentro a API tem "2 etapas" de execução, uma em tempo de execução e outra em tempo de requisição. Assim que a API é executada, todas as dependências são pré-carregadas e executadas, deixando disponível somente quando for necessário.

Então é bem comum ver coisas como a adapter acima, ser declarada da seguinte forma, aqui com mais contexto e também os nomes das variáveis.

const Adapter = dependencies => {
  const {
    services: {
      jestorService
    },
    tables: {
        bankAccount,
        customer,
        guaranteeSignee,
        proposalGuarantee,
    }
  } = dependencies;
 
    
  // é assim que se faz métodos privados em JS
  const getBody = (item) => JSON.parse(item.body);
  const getId = (item, table) => item.data[`id_${table}`];

  const createGuaranteeProposal = async (proposalBody) => {
    const proposalGuaranteeResponse = await jestorService.createItem(proposalGuarantee, proposalBody);
    return getId(getBody(proposalGuaranteeResponse), proposalGuarantee);
  };

  const createCustomer = async (customerBody) => {
    const customerResponse = await jestorService.createItem(customer, customerBody);
    return getId(getBody(customerResponse), customer);
  };

  const createBankAccount = async (bankAccountBody) => {
    const bankAccountResponse = await jestorService.createItem(bankAccount, bankAccountBody);
    return getId(getBody(bankAccountResponse), bankAccount);
  };

  const createSignee = async (signeeBody) => {
    const signeeResponse = await jestorService.createItem(guaranteeSignee, signeeBody);
    return getId(getBody(signeeResponse), guaranteeSignee);
  };

    
  // E aqui deixamos as coisas publicas
  return {
    createBankAccount,
    createCostumer,
    createGuaranteeProposal,
    createSignee,
  }
};

Esse contexto é super importante para enteder as 3 funções(não eram só 3 funções, são várias, repetindo as mesmas instruções) e os problemas que ela tem.

A use-case recebe a adapter acima com o retorno do objeto, pronto para uso, então basta chamar o createSignee por exemplo para que ele faça da tratativa e mantenha uma resposta concisa. (Nos casos que forem necessários a adapter pode chamar um serializer, para modificar algum dado antes da chamada da service. )

Na nossa arquitetura adapters isolam regras específicas para serviços externos de forma que a leitura de nossas use-cases pareçam comandos simples, tal qual uma lista de tarefas.

Então um use-case para o createCostumer, por exemplo, em que recebe a requisição, comunica o banco e o serviço externo, seria da seguinte forma:

const UseCase = dependencies => {

  const {
    adapters: {
        adapter
    },
    repositories: {
        users
    }
  } = dependencies;
  
  // lembra dos contextos? o Finish sinaliza o contexto final da request
  const createCostumer = (data, onFinish) => {
      try {
          // omiti a query p/ simplificar
          const user = users.find(data);

          if (user) return onFinish.BAD_REQUEST({
              message: 'Usuário já existe'
          });
          
          await Promise.all([
            users.insert(data),
            adapter.createCostumer(data)
          ]);
          
   		  onFinish.CREATED();
      } catch(err) {
        onFinish.SERVICE_UNAVAILABLE({
        	message: 'Something bad happens'
      	});
      }
  }

  return {
    createCostumer,
  }
};

Então esse é um modelo ideal de nossa use-case, elas sempre são chamadas pelas controllers que separam framework da aplicação. Hoje conseguimos trocar o nosso framework web se necessário em questão de poucas horas ou até minutos.

Não vou detalhar muito mais do que isso, porém farei uma live na Twitch para explicar isso, já que a anterior pelo visto não está mais lá. Com detalhes melhores e como fazer esse tipo de isolamento funcionar, talvez fique mais fácil explicar o que está envolvido e algumas decisões e também dá para ter uma ideia, aqui nesse texto um pouco desatualizado, porém com os conceitos estão nele.

Reduzindo impacto

Agora que mostrei como é a interação e uso do adapter em uma use-case, vamos para os detalhes sobre a implementação mencionada no Twitter e onde especialmente falaram que eu era da igreja do DRY.

Acontece que a implementação de criação, seja do customer, signee, partner e etc. é semelhante quase todas as vezes, muda somente a tabela que será efetuada a criação.


E aqui vou abrir, um pequeno espaço, principalmente por conta do typo (Jestor) e explicar o que rola, o Jestor é um serviço externo e sim eles tem esse nome o adapter é camada anterior da chamada desse serviço, ali adaptamos os dados para a integração entre os serviços deles e o nosso. Então não é exatamente um typo.

O Jestor funciona com o conceito de tabelas e CRUD básico, porém internamente ele tem automações e outras integrações. Se tornou um facilitador em nossa operação por conseguirmos replicar nossas ações da base para ele como um sistema de backoffice.


Voltando.

Bastava uma função para a criação do item na tabela e nada impediria a tabela ser referenciada no user-case. Algo como:

const UseCase = dependencies => {

  const {
    adapters: {
        adapter
    },
    repositories: {
        users
    },
    tables: {
        costumer
    }
  } = dependencies;
  
  // lembra dos contextos? o Finish sinaliza o contexto final da request
  const createCostumer = (data, onFinish) => {
      try {
          // omiti a query p/ simplificar
          const user = users.find(data);

          if (user) return onFinish.BAD_REQUEST({
              message: 'Usuário já existe'
          });

          // o ideal é quando chega quase formar parágrafos
          await Promise.all([
            users.insert(data),
            adapter.create(costumer, data)
          ]);
          
   		  onFinish.CREATED();
      } catch(err) {
        onFinish.SERVICE_UNAVAILABLE({
        	message: 'Something bad happens'
      	});
      }
  }

  return {
    createCostumer,
  }
};

No Adapter uma única função de create seria da seguinte forma:

const Adapter = dependencies => {
  // resumindo código
  const create = (table, body) => {
      const response = await jestorService.createItem(table, body);
    return getId(getBody(response), table);
  }
  // aqui o retorno do adapter anterior
};

Fazer isso seria o melhor caminho.

Olha como quase forma uma frase adapter create costumer data.

Mas a realidade sempre se impõe e já temos várias outras funções da adapter sendo usadas no projeto e ao menos para reduzir a bagunça do adapter original eu precisava fazer algo.

Outro problema importante de se mencionar é que ao desviar desse ideal, foi criado diversos async/await que é um sintaxe sugar da promises, aumentando o custo de processamento dessas chamadas.

Refatorando

Essa aqui é uma parte um pouco controversa, principalmente por alguns que com falta do contexto julgaram mal á explicação e o código em tweets seguintes. Como a culpa de uma mensagem mal entendida é sempre do emissor, criei esse texto para detalhar isso.

Esse Adapter em questão é chamado em diversas use-cases do projeto.

Modificar p/ usar um único método create, lembrando que tem updates, gets e deletes na mesma situação, seria algo que levaria dias, desviando o foco da funcionalidade a ser entregue na sprint.

Aqui a solução é uma só, abre um issue para fazer esse refactory mais tarde e usar isso de uma forma melhor. Como criar da melhor forma seria impossível, alguma melhoria poderia ser feita. Comecei pela parte que faltava, os testes unitários.

E aqui é importante deixar claro que esse problema todo aconteceu, por minha responsabilidade, eu aprovei o PR sem testes, com a repetição de código que só aumentava e ao informar o dev envolvido aceitei os motivos pelo trabalho fora dos padrões que foram entregues, para evitar que outras entregas ainda atrasadas ficassem ainda mais atrasadas.

Então vamos lá criar os testes, para garantir que as mesmas entradas produzam as mesmas saídas. Feito os testes únitários, é basicamente alguns pontos que estão nesse outro texto, vamos para o mudança do código em si, mantendo a saída das mesmas funções.

A adapter ficou algo mais ou menos assim:

 const Adapter = dependencies => {
  // resumindo código
  const jestorCreateItem = async (table, body) => {
    const response = await jestorService.createItem(table, body);
    return getId(getBody(response), table);
  };

   const createGuaranteeProposal = body => jestorCreateItem(proposalGuarantee, body);
  const createCustomer = body => jestorCreateItem(customer, body);
  const createBankAccount = body => jestorCreateItem(bankAccount, body);
  const createSignee = body => jestorCreateItem(guaranteeSignee, body);

  // E aqui deixamos as coisas publicas
  return {
    createBankAccount,
    createCostumer,
    createGuaranteeProposal,
    createSignee,
  }
};

Ainda longe do ideal, com uma única função, porém deu para ampliar os testes unitários, diminuir o tempo de execução deles e já ter uma redução da repetição de código.

Algo que dá para ser visto nessas imagens.

Sim a forma que usamos injeção de dependencias, esse controle de como é declarado o código, impacta diretamente no nosso tempo de teste.

E a busca por melhoria é sempre alta nesse ponto.

Comentários Finais

Segue alguns modelos mentais para entender as decisões por trás da construção da nossa API.

Primeiro o produto nunca está realmente pronto, são diversas interações e os erros são aceitos, mesmo que isso possa deixar alguém puto e se está puto corrija da melhor forma que conseguir.

A ideia é sempre entregar um código melhor do que o anterior que foi encontrado.

Testes unitários é a ferramenta que mostra se algo está sendo mal ou bem construído, se está difícil de testar tem problemas na implementação, se está tomando tempo da construção, tem algo errado e até mesmo a funcionalidade ou arquitetura deve ser revista.

Código evolui com as necessidades do negócio, não dá para acreditar que o software de hoje vai atender as necessidade de amanhã, porém pequenas peças bem construídas facilitam a construção e evolução do todo.

Cada função é uma dessas pequenas peças, que bem testadas podem ser reutilizadas em diferentes cenários e cenários únicos devem estar em contextos únicos, delimitado pelas as use-cases e em algumas controllers.

Services, Adapters, Repositories e Serializers existem para compartilhamento então devem ser pensados como uma API e ter as mesmas preocupações com break changes, minors e patch versions.

Break Changes são ruins, se mal planejadas e devem ser evitadas, ou consideradas nas implementações em fases.

Jamais a API deve ser responsável por algum estado da requisição, ela é como um encanamento onde a água flui. Os dados são essas águas e devem sempre que possíveis ser imutáveis, por isso raramente será encontrado algo diferente de uma constno código. Se necessário dados podem ser gerados de um dado anterior, mas modificação deve ser evitada.  

Entregar rápido sempre tem um preço e as vezes é só a entrega bugs em produção mais rápido. O segredo está em alavancar as nossas "peças de lego" para atender negócio e não os bugs.

E por último e de igual importante o código precisa ser lido, quase próximo de uma receita de bolo. Então escrever e reler o que escreveu é sempre importante.


Espero que tenha esclarecido, qualquer dúvida, correção e sugestão me chama no xTwitter.