Acelerando a criação de aplicações e serviços com Replicante

Neste artigo, pretendo apresentar uma nova ferramenta para facilitar a geração de projetos a partir de templates. Um processador de templates chamado Replicante. Meu primeiro projeto open-source.

Nos parágrafos abaixo, vou tentar explicar as motivações que me levaram a implementar esse projeto, e como ele pode ser usado para facilitar a vida em algumas tarefas banais que acabam consumindo nosso tempo.

CTRL+C, CTRL+V Plus

De vez em quando temos que começar um novo projeto de software. E muitas vezes é empolgante: é uma oportunidade de fazer diferente, de testar novas abordagens, novas ferramentas e tecnologias.

Mas algumas vezes o novo projeto é muito parecido com outro que fizemos no passado, e usá-lo como template é tentador. É só um CTRL+C e um CTRL+V, e uns replaces, e depois apagar o que for irrelevante para o novo projeto.

Muita gente faz isso, e eu mesmo fiz esse processo inúmeras vezes. E mesmo que você crie um projeto template, fazer esse processo de cópia manualmente é chato, trabalhoso, e suscetível a falhas.

Reconhecendo que este é um padrão recorrente no mundo do software, apesar de são comentado, comecei a refletir se não seria possível automatizar os passos desse processo, tornando-o quase trivial.

Automatizando a transformação de templates

Imagine que você trabalha numa agência que produz sites e outros projetos de software. Você quer que cada projeto seja único, mas tem que pagar as contas. E então, você quer reaproveitar o máximo de trabalho possível de projetos anteriores.

Uma abordagem bem provável seria deixar preparado um boilerplate muito completo, totalmente customizável, e se basear nele toda vez que for começar um projeto novo. O cenário de um freelancer não é muito diferente disso.

Cada stack tem ferramentas particulares para geração de projetos. NodeJS, Ruby, Python, Go, C#, Java. Cada uma funciona de um jeito.

Mas e se pudéssemos implementar um projeto template, em qualquer tecnologia, e usar uma ferramenta externa para gerar nossos projetos?

E se essa ferramenta fosse fácil de usar?

E se ela fosse fácil de integrar aos nossos pipelines de integração contínua?

Replicante

E assim surgiu o Replicante, um processador de templates fácil de usar, que roda direto no seu terminal.

Um dos objetivos iniciais do projeto foi que ele deveria operar totalmente via configuração. O processo teria que ser declarativo.

Diagrama representando o processo básico do replicante, aplicando uma receita a um template para gerar um novo projeto

Replicação de um template

Por isso, todo processo de replicação começa com um projeto template e uma receita como esta:

{
  "replicantName": "NovoSuperProjeto",
  "fileNameReplacements": [],
  "sourceCodeReplacements": []
}

Como podemos ver, o novo projeto é denominado replicante, e tem um nome. Além disso temos outras duas coleções: as substituições em nomes de arquivos, e as de código fonte.

Cada uma delas é alimentada elementos que indicam regras de substituição, como esse:

{ "from": "TermoOriginal", "to": "NovoTermo" }

As regras definidas na coleção fileNameReplacements afetarão nomes de arquivos e pastas. Vejamos um exemplo simples: digamos que nosso template possui uma pasta chamada ClienteX, que funciona como namespace para todos os fontes lá dentro. Num projeto C#, seria algo assim:

`ClienteX
|---Services
|---Controllers
|---...

E para complementar cada arquivo fonte .cs dentro da pasta cliente vai incluir o diretório nos namespaces assim:

public namespace ClienteX.Services
{
    public class MeuServico{...}
}

Se quisermos que todo novo projeto tenha o nome do cliente como namespace, é só fazer a seguinte configuração e voilà:

{
  "replicantName": "NovoSuperProjeto",
  "fileNameReplacements": [
      { "from": "ClienteX", "to": "BurgerKing" }
  ],
  "sourceCodeReplacements": [
      { "from": "ClienteX", "to": "BurgerKing" }
  ]
}

A aplicação desta receita vai resultar no seguinte:

`BurguerKing
|---Services
|---Controllers
|---...
public namespace BurguerKing.Services
{
    public class MeuServico {...}
}

A ferramenta em si é um app de linha de comando implementado com um mix de Javascript e TypeScript usando um toolkit chamado Gluegun. Você pode conferir meu artigo anterior onde falo sobre como foi implementar o Replicante com Gluegun.

A utilização dele é bem simples:

replicante create \
  c:/projects/template \ # caminho para a pasta do template
  c:/receitas/receita-bk.json \ # caminho para a receita
  --target=c:/projects/bk # caminho de destino, onde será gerado o projeto

Gluegun

A CLI do Replicante foi desenvolvida usando um toolkit chamado Gluegun. Você pode conferir meu artigo sobre como foi o desenvolvimento do Replicante usando essa ferrementa acessando este link.

Exemplo lúdico: Humanizer -> Martianizer

Que tal um exemplo mais excêntrico? Digamos que os autores da biblioteca Humanizer (excelente biblioteca, aliás) acordassem um dia com a brilhante ideia de renomear a biblitoeca para Martianizer (já pensando nos nossos amigos marcianos).

Isso implica que todas as APIs teriam que ser ajustadas, e os projetos renomeados. Nessa biblioteca, o termo Humanize está para todo lado. Nos métodos, nomes das classes, namespaces, nomes de arquivos, projeto, solution e arquivos de configuração.

using System;
using System.Collections.Generic;
using Humanizer.Configuration;

namespace Humanizer
{
    /// <summary>
    /// Humanizes an IEnumerable into a human readable list
    /// </summary>
    public static class CollectionHumanizeExtensions
    {
        /// <summary>
        /// Formats the collection for display, calling ToString() on each object and
        /// using the default separator for the current culture.
        /// </summary>
        public static string Humanize<T>(this IEnumerable<T> collection)
        {
            return Configurator.CollectionFormatter.Humanize(collection);
        }
    
        //...

Para transformar esse projeto no Martianizer, bastaria aplicar a seguinte receita:

{
  "replicantName": "Martianizer",
  "templateName": "martianizr-generator",
  "fileNameReplacements": [
    { "from": "Human", "to": "Martian" }
  ],
  "sourceCodeReplacements": [    
    { "from": "HUMAN", "to": "MARTIAN" }, 
    { "from": "Human", "to": "Martian" },
    { "from": "human", "to": "martian" }
  ]
}

Essas poucas substituições cobrem todas as ocorrências no repositório de termos como humanize, humanizing, humanizer, humanization, dehumanization, humanized e Humanizr. E os resultados são bem promissores. O projeto já nasce correto, compilando, e “marcianizado”:

using System;
using System.Collections.Generic;
using Martianizer.Configuration;

namespace Martianizer
{
    /// <summary>
    /// Martianizes an IEnumerable into a human readable list
    /// </summary>
    public static class CollectionMartianizeExtensions
    {
        /// <summary>
        /// Formats the collection for display, calling ToString() on each object and
        /// using the default separator for the current culture.
        /// </summary>
        public static string Martianize<T>(this IEnumerable<T> collection)
        {
            return Configurator.CollectionFormatter.Martianize(collection);
        }

        //...

E a árvore de arquivos sai correta, com todas as transformações aplicadas:

Árvore de arquivos marcianizada

Árvore de arquivos marcianizada

Tipos comuns de ajuste

Existem várias situações e vários tipos de arquivo que precisamos ajustar para transformar um template num projeto novo, como:

  • Arquivos fontes
  • Arquivos de configuração
  • Arquivos de pipeline de CI/CD

Todos esses casos podem se beneficiar dessas regras de substituição.

Processamento de templates

O Replicante processa os templates em duas etapas, como podemos ver no diagrama a seguir:

Diagrama mostrando as etapas de processamento de um template

Processamento de um template

Na primeira etapa, ele gera um projeto virtual, em que cada arquivo tem seu caminho transformado num template para processamento posterior, como no exemplo a seguir:

ClienteX/Services/MeuServico -> BurgerKing/Services/MeuServico

Esse passo pode parecer desnecessário à primeira vista, mas é permite a adição de funcionalidades mais avançadas, como o uso de variáveis no template.

Por exemplo, o nome do replicante é disponibilizado como uma variável <<: name :>>, e pode ser usado assim:

{
  "replicantName": "BurgerKing",
  "fileNameReplacements": [
      { "from": "ClienteX", "to": "<<: name :>>" }
  ],
  "sourceCodeReplacements": [
      { "from": "ClienteX", "to": "<<: name :>>" }
  ]
}

Além da variável <<: name :>>, também há outras variações, como nameLower, nameUpper, entre outras.

No momento, estas são as únicas variáveis suportadas, mas no futuro outras podem ser adicionadas, como datas, por exemplo.

Por fim, na segunda etapa, estas variáveis são substituídas por seus valores finais, permitindo a cópia dos arquivos para o destino final.

Uma vez copiados, são aplicadas as regras de substituição de conteúdo nesses arquivos.

Por fim, os arquivos estáticos são copiados para lá.

Qualquer projeto é um template

Dada a natureza de funcionamento do Replicante, criar um template pode até ser desnecessário.

Uma alternativa passa a ser copiar um projeto anterior, provavelmente o mais parecido com o que desejamos criar, e modificá-lo a partir daí. Ou seja, qualquer projeto pode servir como template.

Basta entender quais termos precisamos ajustar, criar uma receita, e aplicar.

Templates vivos

Uma abordagem que podemos facilitando adotar, é a criação de uma aplicação template. Esta aplicação pode então servir de base para a criação de outras.

Mas ela não é apenas um template, mas o que eu chamo de template vivo, que possui todos os testes automatizados necessários, com exemplos de implementação dos principais recursos, pipeline de CI/CD, análise estática de código, com ferramentas como SonarQube, por exemplo, e documentação.

Já está claro neste ponto, que qualquer aplicação pode servir como um template vivo, mas criar uma aplicação exclusivamente com este propósito pode ser uma boa ideia. Tudo vai depender do cenário.

Finalizando

O Replicante pode ser instalado via NPM usando o comando npm i -g replicante, e o comando de ajuda vai te guiar pelo processo.

Implementar essa ferramenta me trouxe vários aprendizados, entre eles, a humildade de admitir que as vezes a gente realmente faz esses CTRL+C, CTRL+V descarados para ganhar tempo.

Já integrei o replicante num pipeline do Azure DevOps para criar gerar novos microserviços com base em um template de Arquitetura Limpa, e funciona lindamente.

Outras opções para compartilhar:
comments powered by Disqus