Autoload paths no Rails 6

Fala, gente!

Hoje vamos falar um pouco sobre como o Rails 6 trabalha quando o assunto é o carregamento dos arquivo, classes e módulos em uma aplicação.

Antes de mais nada, o Rails 6 introduziu o "modo Zeitwerk", que em resumo é utilizar por padrão uma gem (https://github.com/fxn/zeitwerk) para fazer toda essa parte de autoload e reload da app que antes era feita pelo próprio core do framework.

Não por acaso, o criador da Gem faz parte do Rails Core Team e nesse post ele explica os motivos que o levaram a criar essa gem em paralelo.

Voltando ao nosso assunto, achei interessante abordá-lo pois, principalmente para os iniciantes, a prática de estender o Rails criando pastas e arquivos próprios não é comum, mas por outro lado, quando a aplicação começa a crescer ou mesmo quando começamos a aplicar boas práticas ou alguns Design Patterns, isso é praticamente inevitável. Sendo assim, o melhor é entender e fazer do jeito certo quando for necessário. Então vamos lá!

Documentação

Bom, a primeira coisa que acredito ser importante sobre esse assunto é você saber que pode estudar diretamente pela documentação, afinal ela sempre será o melhor lugar adquirir novos conhecimentos.

Tradicional vs Automatizado

Em uma aplicação ruby tradicional, quando criamos arquivos e declaramos módulos/classes/constantes, ao utilizá-los em outro arquivo faz-se necessário o uso do require.

Sendo assim, imagine uma aplicação Rails com centenas de arquivos sendo necessário pelo menos um require dentro de arquivos como controllers, models, helpers, etc... para que eles funcionem como o esperado. Acho que você concorda que isso seria algo indesejado.

Apesar de parecer algo besta muitos frameworks ainda não possuem isso resolvido de forma trivial... basta olhar para o aclamado React... -- tomei a liberdade de procurar qualquer projeto no github para servir de exemplo -- Veja que nele necessitamos carregar arquivo por arquivo que queremos trabalhar. Nesse caso, tome import!

O Rails por outro lado, desde suas primeiras versões (comecei a usar na 2.3 e já era assim) traz o autoload automatizado, e isso implica em coisas boas e ruins.

De forma resumida, a boa é que é uma preocupação a menos para o desenvolvedor ficar controlando o que importar ou não importar, além também de tornar código fica mais limpo. Por outro lado carregar tudo em memória e gerir tudo ao mesmo tempo não é nada fácil, afinal, conflitos podem ocorrer e a app pode acabar consumindo muita memória, mas isso é assunto para um outro momento. O que precisamos entender aqui é que um dos benefícios do Rails ser todo padronizado com as pastas, etc (nosso famoso Convention Over Configuration), é que isso facilita justamente saber o que carregar ou não de forma automatizada, e é aí que entra a gem Zeitwerk que foi criada apenas com o objetivo de gerir o carregamento automatizado de arquivos em projetos ruby.

Agora que temos todas essa historinha em mente, talvez a principal pergunta seja... -- Preciso criar algumas libs e extender algumas gems do projeto, como devo proceder? --

Pois bem, foi pensando nisso que escrevi essas cinco regrinhas básicas para o entendimento dos loadpaths no Rails 6.

Saiba dessas 5 regras e seja feliz!

Após ler a documentação citada, além de alguns outros blogs cheguei a seguinte conclusão de que basicamente você precisa saber de apenas 5 coisas:

  1. Você pode consultar o Autoload Path atual através de ActiveSupport::Dependencies.autoload_paths.
  2. Com exceção das pastas assets, javascripts e views todas as outras pastas dentro de /app são carregadas de forma automática.
  3. Para que um arquivo seja carregado pelo autoload path de forma válida, o nome do módulo/classe deve ser igual ao do arquivo em CamelCase.
  4. Sempre que quiser criar uma subpasta na pasta raiz app/, a mesma deve fazer parte do namespace do seu módulo/classe.
  5. Tudo que é carregado de forma automática fica disponível na raiz do namespace Object.

Sabendo disso, vamos destrinchar cada uma...

1. Você pode consultar o Autoload Path atual através de ActiveSupport::Dependencies.autoload_paths

Ok, a primeira coisa é verificar o que é carregado por padrão em sua aplicação. Sendo assim, digite no terminal...

/> rails r 'puts ActiveSupport::Dependencies.autoload_paths'

/Users/jacksonpires/Projects/test-app/app/channels
/Users/jacksonpires/Projects/test-app/app/controllers
/Users/jacksonpires/Projects/test-app/app/controllers/concerns
/Users/jacksonpires/Projects/test-app/app/helpers
/Users/jacksonpires/Projects/test-app/app/jobs
/Users/jacksonpires/Projects/test-app/app/mailers
/Users/jacksonpires/Projects/test-app/app/models
/Users/jacksonpires/Projects/test-app/app/models/concerns
...
/Users/jacksonpires/Projects/test-app/test/mailers/previews

A saída deve ser algo como isso que vimos acima. Ou seja, essas pastas e seus conteúdos são carregados de forma automática pelo Rails.

2. Com exceção das pastas assets, javascripts e views todas as outras pastas dentro de /app são carregadas de forma automática

Foi exatamente isso que vimos quando rodamos o comando rails r 'puts ActiveSupport::Dependencies.autoload_paths' , ou seja, assets, javascripts e views não estão presentes na lista de pastas carregadas!

Vamos aproveitar e fazer um teste criando uma pasta dentro de app chamada services, conforme imagem abaixo.

enter image description here

Depois rodamos o comando novamente para verificar os autoload paths...

/> rails r 'puts ActiveSupport::Dependencies.autoload_paths'

/Users/jacksonpires/Projects/test-app/app/channels
/Users/jacksonpires/Projects/test-app/app/controllers
/Users/jacksonpires/Projects/test-app/app/controllers/concerns
/Users/jacksonpires/Projects/test-app/app/helpers
/Users/jacksonpires/Projects/test-app/app/jobs
/Users/jacksonpires/Projects/test-app/app/mailers
/Users/jacksonpires/Projects/test-app/app/models
/Users/jacksonpires/Projects/test-app/app/models/concerns
/Users/jacksonpires/Projects/test-app/app/services
...
/Users/jacksonpires/Projects/test-app/test/mailers/previews

Prontinho, podemos perceber que a pasta services já aparece lá.

Caso no seu teste não apareça, tente parar o spring com spring stop, visto que algumas vezes ele não limpa o cache.

3. Para que um arquivo seja carregado pelo autoload path de forma válida, o nome do módulo/classe deve ser igual ao do arquivo em CamelCase

Para provar isso, crie um arquivo my_service.rb dentro da pasta app/services conforme imagem abaixo.

enter image description here

Dentro do arquivo, coloque o conteúdo abaixo...

# app/services/my_service.rb

module MyService
  HELLO = "Hello from app/services/my_service.rb"
end

Agora teste rodando o comando...

/> rails r 'puts MyService::HELLO'
#> Hello from app/services/my_service.rb

Viu só! Facinho! 🙂

Lembre-se que nesse caso, o nome da classe/módulo deve obedecer a regra de ser o mesmo nome do arquivo em formato CamelCase. Sendo assim, se tiver alguma dúvida em como o nome deve ficar, você poder usar o comando abaixo para verificar.

/> rails r 'puts "my_service".camelize'
#> MyService

4. Sempre que quiser criar uma subpasta na pasta raiz app/ , a mesma deve fazer parte do namespace do seu módulo/classe

Isso mesmo, para esse exemplo vamos criar a subpasta app/services/my_services e dentro dela criar um arquivo my_second_service.rb, veja:

enter image description here

Agora adicione o conteúdo abaixo no arquivo.

# app/services/my_services/my_second_service.rb

module MyServices::MySecondService
    HELLO =  "Hello from app/services/my_services/my_second_service.rb"
end

Lembre-se que o conteúdo acima também pode ser escrito assim:

module  MyServices
    module  MySecondService
        HELLO =  "Hello from app/services/my_services/my_second_service.rb"
    end
end

Por fim, teste mais uma vez e veja que tudo continua funcionando.

/> rails r 'puts MyServices::MySecondService::HELLO'
#> Hello from app/services/my_services/my_second_service.rb

5. Tudo que é carregado de forma automática fica disponível na raiz da classe Object

Para provar isso, basta a gente entrar no rails console e digitar Object::MyServices::MySecondService::H e depois pressione TAB e perceba que ele auto-completa o HELLO.

Ahh, claro que se você digitar Object:: e pressionar TAB duas vezes, daí será questionado se quer mostrar todas as possibilidades e respondendo 'yes' poderá ver que os módulos que usamos nesse artigo estão todos disponíveis lá! 🙂

Finalizando

Por fim, algumas pessoas podem perguntar... -- Ahhh, mas eu quero colocar uma biblioteca pra ser carregada a partir de lib/minhas_libs ao invés de app/minhas_libs , como faço? --

A resposta é simples... nesse caso você deve adicionar a pasta no módulo LoadPath do Rails através do arquivo config/application.rb, usando o comando config.autoload_paths, mas na documentação do Rails ele informa que isso não é uma boa prática, ou seja, o ideal é usar tudo em app/ mesmo, ok?

É isso, gente!
Espero que tenham gostado!
Até a próxima! 😉