Build Dumb, Refactor Smart: Como massagear problemas do código Ruby on Rails

Publicados: 2022-03-11
Daniel Lewis é desenvolvedor profissional de Ruby on Rails há mais de 4 anos, trabalhando em cerca de uma dúzia de aplicações web de alto tráfego, muitas delas através da Toptal.

Às vezes, os clientes nos dão solicitações de recursos que realmente não gostamos. Não é que não gostemos de nossos clientes — nós os amamos. Não é que não gostemos do recurso - a maioria dos recursos solicitados pelos clientes está perfeitamente alinhada com suas metas de negócios e receita.

Às vezes, não gostamos de uma solicitação de recurso porque a maneira mais fácil de resolvê-la é escrever um código ruim e não temos uma solução elegante em mente. Isso enviará muitos de nós desenvolvedores Rails em buscas infrutíferas através do RubyToolbox, GitHub, blogs de desenvolvedores e StackOverflow procurando por uma gem ou plugin ou código de exemplo que nos faça sentir melhor sobre nós mesmos.

Às vezes, não gostamos de uma solicitação de recurso porque a maneira mais fácil de resolvê-la é escrever um código ruim.
Tweet

Refatorar código Ruby on Rails

Bem, estou aqui para lhe dizer: não há problema em escrever código ruim. Às vezes, um código Rails ruim é mais fácil de refatorar em um código bonito do que uma solução mal pensada implementada sob uma crise de tempo.

Este é o processo de refatoração do Rails que gosto de seguir ao remover problemas das minhas horríveis soluções de band-aid:

Este processo de refatoração do Ruby on Rails é o que sigo para resolver os problemas do Rails.

Para uma perspectiva alternativa, aqui está o log de confirmação do Git para um recurso que foi refatorado passo a passo:

Este log demonstra uma refatoração do Rails que foi feita passo a passo.

E aqui está outro artigo interessante sobre refatoração em larga escala de um colega da rede Toptal.

Vamos ver como é feito.

As visualizações

Etapa 1. Comece nas Visualizações

Digamos que estamos iniciando um ticket para um novo recurso. O cliente nos diz: “Os visitantes devem poder visualizar uma lista de projetos ativos na página de boas-vindas”.

Esse ticket requer uma alteração visível, portanto, um local razoável para iniciar o trabalho seria nas Visualizações. O problema é direto e que todos nós fomos treinados para resolver várias vezes. Vou resolvê-lo da maneira errada e demonstrar como refatorar minha solução nas áreas apropriadas. Resolver um problema O Caminho Errado pode nos ajudar a superar a dificuldade de não saber a solução certa.

Para começar, suponha que temos um modelo chamado Project com um atributo booleano chamado active . Queremos obter uma lista de todos os Projects em que active é igual a true , para que possamos usar Project.where(active: true) e fazer um loop sobre ele com each bloco.

 app/views/pages/welcome.haml: %ul.projects - Project.where(active: true).each do |project| %li.project= link_to project_path(project), project.name

Eu sei o que você está dizendo: “Isso nunca passaria em uma revisão de código” ou “Meu cliente certamente me demitiria por isso”. Sim, esta solução quebra a separação de preocupações Model-View-Controller, pode resultar em chamadas de banco de dados perdidas que são difíceis de rastrear e pode se tornar difícil de manter no futuro. Mas considere o valor de fazê-lo da maneira errada :

  1. Você pode obter essa alteração no teste em menos de 15 minutos.
  2. Se deixado, este bloco é fácil de armazenar em cache.
  3. Corrigir este problema do Rails é simples (pode ser dado a um desenvolvedor júnior).

Etapa 2. Parciais

Depois de fazer isso da maneira errada , me sinto mal comigo mesmo e quero isolar meu código ruim. Se essa mudança fosse claramente apenas uma preocupação da camada View, eu poderia refatorar minha vergonha em uma parcial.

 app/views/pages/welcome.haml: = render :partial => 'shared/projects_list' app/views/shared/projects_list.haml: %ul.projects - Project.where(active: true).each do |project| %li.project= link_to project_path(project), project.name

Isso é um pouco melhor. Claramente, ainda estamos cometendo o erro de uma consulta Model em uma View, mas pelo menos quando um mantenedor chegar mais tarde e ver minha parcial horrível, eles terão uma maneira direta de resolver esse problema de código Rails em particular. Consertar algo obviamente estúpido é sempre mais fácil do que consertar uma abstração mal implementada e com erros.

Consertar algo obviamente estúpido é sempre mais fácil do que consertar uma abstração mal implementada e com erros.
Tweet

Etapa 3. Auxiliares

Helpers no Rails são uma forma de criar uma DSL (Domain Specific Language) para uma seção de suas Views. Você precisa reescrever seu código usando content_tag's em vez de haml ou HTML, mas você obtém o benefício de poder manipular estruturas de dados sem que outros desenvolvedores olhem para você por 15 linhas de código View não imprimível.

Se eu fosse usar auxiliares aqui, provavelmente refatoraria a tag li .

 app/views/shared/projects_list.haml: %ul.projects - Project.where(active: true).each do |project| = project_list_item(project) app/helpers/projects_helper.rb: def project_list_item(project) content_tag(:li, :class => 'project') do link_to project_path(project), project.name end end

Os Controladores

Etapa 4. Mova-o para os controladores

Talvez seu código horrível não seja apenas uma preocupação do View. Se o seu código ainda cheira mal, procure por consultas que você possa fazer a transição das Views para os Controllers.

 app/views/shared/projects_list.haml: %ul.projects - @projects_list.each do |project| = project_list_item(project) app/controllers/pages_controller.rb: def welcome @projects = Project.where(active: true) end

Etapa 5. Filtros do Controlador

A razão mais óbvia para mover o código para um controlador before_filter ou after_filter é para o código que você duplicou em várias ações do Controlador. Você também pode mover o código para um filtro do Controlador se quiser separar a finalidade da ação do Controlador dos requisitos de suas exibições.

 app/controllers/pages_controller.rb: before_filter :projects_list def welcome end def projects_list @projects = Project.where(active:true) end

Etapa 6. Controlador de aplicativos

Presumindo que você precisa que seu código apareça em todas as páginas, ou você deseja disponibilizar as funções auxiliares do Controlador para todos os controladores, você pode mover sua função para o ApplicationController. Se as alterações forem globais, convém modificar o layout do aplicativo também.

 app/controllers/pages_controller.rb: def welcome end app/views/layouts/application.haml: %ul.projects - projects_list.each do |project| = project_list_item(project) app/controllers/application_controller.rb: before_filter :projects_list def projects_list @projects = Project.where(active: true) end

Os modelos

Etapa 7. Refatorando para o modelo

Como diz o lema do MVC: Fat Model, Skinny Controllers e Dumb Views . Espera-se que refatoremos tudo o que pudermos no Modelo, e é verdade que a funcionalidade mais complexa acabará se tornando associações de modelo e métodos de modelo. Devemos sempre evitar formatar/visualizar coisas no Modelo, mas é permitido transformar dados em outros tipos de dados. Nesse caso, a melhor coisa para refatorar no Model seria a cláusula where(active: true) , que podemos transformar em um escopo. Usar um escopo é valioso não apenas porque torna a chamada mais bonita, mas se decidirmos adicionar um atributo como delete ou outdated , podemos modificar esse escopo em vez de procurar todas as nossas cláusulas where .

 app/controllers/application_controller.rb: before_filter :projects_list def projects_list @projects = Project.active end app/models/project.rb: scope :active, where(active: true)

Etapa 8. Filtros de modelo

Não temos um uso específico para os after_save filters before_save um modelo neste caso, mas o próximo passo que costumo dar é mover a funcionalidade de controladores e métodos de modelo para filtros de modelo.

Digamos que tivéssemos outro atributo, num_views . Se num_views > 50 , o projeto fica inativo. Poderíamos resolver esse problema na View, mas fazer alterações no banco de dados em uma View é inapropriado. Poderíamos resolvê-lo no Controller, mas nossos Controllers devem ser o mais finos possível! Podemos resolvê-lo facilmente no modelo.

 app/models/project.rb: before_save :deactivate_if_over_num_views def deactivate_if_over_num_views if num_views > 50 self.active = false fi end

Nota: você deve evitar chamar self.save em um filtro de Modelo, pois isso causa eventos de salvamento recursivos, e a camada de manipulação de banco de dados de seu aplicativo deve ser o Controlador de qualquer maneira.

Etapa 9. Bibliotecas

Ocasionalmente, seu recurso é grande o suficiente para garantir sua própria biblioteca. Você pode querer movê-lo para um arquivo de biblioteca porque é reutilizado em muitos lugares, ou é grande o suficiente para que você queira desenvolver separadamente.

Não há problema em armazenar arquivos de biblioteca no diretório lib/ , mas à medida que eles crescem, você pode transferi-los para um RubyGem real! Uma grande vantagem de mover seu código para uma biblioteca é que você pode testar a biblioteca separadamente do seu modelo.

De qualquer forma, no caso de uma Lista de Projetos, poderíamos justificar mover a chamada scope :active do modelo Project para um arquivo de biblioteca e trazê-la de volta para Ruby:

 app/models/project.rb: class Project < ActiveRecord::Base include Activeable before_filter :deactivate_if_over_num_views end lib/activeable.rb: module Activeable def self.included(k) k.scope :active, k.where(active: true) end def deactivate_if_over_num_views if num_views > 50 self.active = false end end end

Nota: o método self.included é chamado quando uma classe Rails Model é carregada e passa no escopo da classe como a variável k .

Conclusão

Neste tutorial de refatoração do Ruby on Rails, levamos menos de 15 minutos e implementamos uma solução e a colocamos em teste para teste do usuário, pronta para ser aceita no conjunto de recursos ou removida. Ao final do processo de refatoração, temos um pedaço de código que estabelece uma estrutura para implementar itens listáveis ​​e ativáveis ​​em vários Modelos que passariam até mesmo pelo processo de revisão mais rigoroso.

Em seu próprio processo de refatoração do Rails, sinta-se à vontade para pular algumas etapas do pipeline se estiver confiante em fazê-lo (por exemplo, pule de View para Controller ou Controller para Model). Apenas tenha em mente que o fluxo de código é de View para Model.

Não tenha medo de parecer estúpido. O que separa as linguagens modernas dos antigos aplicativos de renderização de modelos CGI não é que fazemos tudo do jeito certo todas as vezes — é que dedicamos tempo para refatorar, reutilizar e compartilhar nossos esforços.

Relacionado: Truncamento de carimbo de data/hora: um conto do ActiveRecord do Ruby on Rails