Construa componentes elegantes do Rails com objetos simples do Ruby

Publicados: 2022-03-11

Seu site está ganhando força e você está crescendo rapidamente. Ruby/Rails é sua linguagem de programação preferida. Sua equipe é maior e você desistiu de “modelos gordos, controladores magros” como estilo de design para seus aplicativos Rails. No entanto, você ainda não quer abandonar o uso do Rails.

Sem problemas. Hoje, discutiremos como usar as melhores práticas da OOP para tornar seu código mais limpo, mais isolado e mais desacoplado.

Vale a pena refatorar seu aplicativo?

Vamos começar analisando como você deve decidir se seu aplicativo é um bom candidato para refatoração.

Aqui está uma lista de métricas e perguntas que costumo me fazer para determinar se meu código precisa ou não de refatoração.

  • Testes unitários lentos. Os testes de unidade PORO geralmente são executados rapidamente com código bem isolado, portanto, testes de execução lenta geralmente podem ser um indicador de um design ruim e responsabilidades excessivamente acopladas.
  • Modelos ou controladores FAT. Um modelo ou controlador com mais de 200 linhas de código (LOC) geralmente é um bom candidato para refatoração.
  • Base de código excessivamente grande. Se você tiver ERB/HTML/HAML com mais de 30.000 LOC ou código-fonte Ruby (sem GEMs ) com mais de 50.000 LOC, há uma boa chance de você refatorar.

Tente usar algo assim para descobrir quantas linhas de código-fonte Ruby você tem:

find app -iname "*.rb" -type f -exec cat {} \;| wc -l

Este comando irá pesquisar todos os arquivos com extensão .rb (arquivos ruby) na pasta /app e imprimir o número de linhas. Observe que esse número é apenas aproximado, pois as linhas de comentários serão incluídas nesses totais.

Outra opção mais precisa e informativa é usar as stats da tarefa de rake do Rails, que gera um resumo rápido de linhas de código, número de classes, número de métodos, proporção de métodos para classes e proporção de linhas de código por método:

 bundle exec rake stats +----------------------+-------+-----+-------+---------+-----+-------+ | Name | Lines | LOC | Class | Methods | M/C | LOC/M | +----------------------+-------+-----+-------+---------+-----+-------+ | Controllers | 195 | 153 | 6 | 18 | 3 | 6 | | Helpers | 14 | 13 | 0 | 2 | 0 | 4 | | Models | 120 | 84 | 5 | 12 | 2 | 5 | | Mailers | 0 | 0 | 0 | 0 | 0 | 0 | | Javascripts | 45 | 12 | 0 | 3 | 0 | 2 | | Libraries | 0 | 0 | 0 | 0 | 0 | 0 | | Controller specs | 106 | 75 | 0 | 0 | 0 | 0 | | Helper specs | 15 | 4 | 0 | 0 | 0 | 0 | | Model specs | 238 | 182 | 0 | 0 | 0 | 0 | | Request specs | 699 | 489 | 0 | 14 | 0 | 32 | | Routing specs | 35 | 26 | 0 | 0 | 0 | 0 | | View specs | 5 | 4 | 0 | 0 | 0 | 0 | +----------------------+-------+-----+-------+---------+-----+-------+ | Total | 1472 |1042 | 11 | 49 | 4 | 19 | +----------------------+-------+-----+-------+---------+-----+-------+ Code LOC: 262 Test LOC: 780 Code to Test Ratio: 1:3.0
  • Posso extrair padrões recorrentes na minha base de código?

Desacoplamento em ação

Vamos começar com um exemplo do mundo real.

Finja que queremos escrever um aplicativo que rastreie o tempo para corredores. Na página principal, o usuário pode ver os horários em que digitou.

Cada entrada de tempo tem uma data, distância, duração e informações de “status” relevantes adicionais (por exemplo, clima, tipo de terreno, etc.), e uma velocidade média que pode ser calculada quando necessário.

Precisamos de uma página de relatório que exiba a velocidade média e a distância por semana.

Se a velocidade média da entrada for maior que a velocidade média geral, notificaremos o usuário com um SMS (para este exemplo, usaremos a API RESTful Nexmo para enviar o SMS).

A página inicial permitirá que você selecione a distância, data e tempo gasto na corrida para criar uma entrada semelhante a esta:

Também temos uma página de statistics que é basicamente um relatório semanal que inclui a velocidade média e a distância percorrida por semana.

  • Você pode conferir a amostra online aqui.

O código

A estrutura do diretório do app é algo como:

 ⇒ tree . ├── assets │ └── ... ├── controllers │ ├── application_controller.rb │ ├── entries_controller.rb │ └── statistics_controller.rb ├── helpers │ ├── application_helper.rb │ ├── entries_helper.rb │ └── statistics_helper.rb ├── mailers ├── models │ ├── entry.rb │ └── user.rb └── views ├── devise │ └── ... ├── entries │ ├── _entry.html.erb │ ├── _form.html.erb │ └── index.html.erb ├── layouts │ └── application.html.erb └── statistics └── index.html.erb

Não discutirei o modelo User , pois não é nada especial, pois o estamos usando com o Devise para implementar a autenticação.

Quanto ao modelo Entry , ele contém a lógica de negócios para nosso aplicativo.

Cada Entry pertence a um User .

Validamos a presença dos atributos distance , time_period , date_time e status para cada entrada.

Toda vez que criamos uma entrada, comparamos a velocidade média do usuário com a média de todos os outros usuários do sistema e notificamos o usuário por SMS usando o Nexmo (não discutiremos como a biblioteca Nexmo é usada, embora eu quisesse para demonstrar um caso em que usamos uma biblioteca externa).

  • Amostra de essência

Observe que o modelo Entry contém mais do que apenas a lógica de negócios. Ele também lida com algumas validações e retornos de chamada.

O entries_controller.rb tem as principais ações CRUD (sem atualização). EntriesController#index obtém as entradas para o usuário atual e ordena os registros por data de criação, enquanto EntriesController#create cria uma nova entrada. Não há necessidade de discutir o óbvio e as responsabilidades de EntriesController#destroy :

  • Amostra de essência

Enquanto statistics_controller.rb é responsável por calcular o relatório semanal, StatisticsController#index obtém as entradas para o usuário logado e as agrupa por semana, empregando o método #group_by contido na classe Enumerable do Rails. Em seguida, ele tenta decorar os resultados usando alguns métodos privados.

  • Amostra de essência

Não discutimos muito os pontos de vista aqui, pois o código-fonte é autoexplicativo.

Abaixo está a visualização para listar as entradas para o usuário conectado ( index.html.erb ). Este é o template que será usado para exibir os resultados da ação de índice (método) no controlador de entradas:

  • Amostra de essência

Observe que estamos usando parciais render @entries , para extrair o código compartilhado em um modelo parcial _entry.html.erb para que possamos manter nosso código DRY e reutilizável:

  • Amostra de essência

O mesmo vale para a parcial _form . Em vez de usar o mesmo código com ações (new e edit), criamos um formulário parcial reutilizável:

  • Amostra de essência

Quanto à visualização da página do relatório semanal, statistics/index.html.erb mostra algumas estatísticas e relata o desempenho semanal do usuário agrupando algumas entradas:

  • Amostra de essência

E, finalmente, o auxiliar para entradas, entries_helper.rb , inclui dois auxiliares readable_time_period e readable_speed que devem tornar os atributos mais legíveis:

  • Amostra de essência

Nada extravagante até agora.

A maioria de vocês argumentará que refatorar isso é contra o princípio KISS e tornará o sistema mais complicado.

Então, esse aplicativo realmente precisa de refatoração?

Absolutamente não , mas vamos considerá-lo apenas para fins de demonstração.

Afinal, se você verificar a seção anterior e as características que indicam que um aplicativo precisa de refatoração, fica óbvio que o aplicativo em nosso exemplo não é um candidato válido para refatoração.

Ciclo da vida

Então vamos começar explicando a estrutura do padrão Rails MVC.

Normalmente, ele começa pelo navegador fazendo uma solicitação, como https://www.toptal.com/jogging/show/1 .

O servidor web recebe a solicitação e usa routes para descobrir qual controller usar.

Os controladores fazem o trabalho de analisar solicitações de usuários, envios de dados, cookies, sessões etc. e, em seguida, solicitam ao model que obtenha os dados.

Os models são classes Ruby que conversam com o banco de dados, armazenam e validam dados, executam a lógica de negócios e fazem o trabalho pesado. Visualizações são o que o usuário vê: HTML, CSS, XML, Javascript, JSON.

Se quisermos mostrar a sequência do ciclo de vida de uma requisição Rails, seria algo assim:

Rails desacoplando o ciclo de vida do MVC

O que eu quero alcançar é adicionar mais abstração usando objetos de ruby ​​antigos simples (POROs) e tornar o padrão algo como o seguinte para ações de create/update :

Diagrama de Rails criar formulário

E algo como o seguinte para ações de list/show :

Consulta da lista de diagramas do Rails

Adicionando abstrações de POROs garantiremos a separação total entre responsabilidades SRP, algo que Rails não é muito bom.

Diretrizes

Para alcançar o novo design, usarei as diretrizes listadas abaixo, mas observe que essas não são regras que você deve seguir ao máximo. Pense nelas como diretrizes flexíveis que facilitam a refatoração.

  • Os modelos ActiveRecord podem conter associações e constantes, mas nada mais. Isso significa que não há retornos de chamada (use objetos de serviço e adicione os retornos de chamada lá) e nenhuma validação (use objetos Form para incluir nomenclatura e validações para o modelo).
  • Mantenha os controladores como camadas finas e sempre chame objetos de serviço. Alguns de vocês perguntariam por que usar controladores já que queremos continuar chamando objetos de serviço para conter a lógica? Bem, os controladores são um bom lugar para ter o roteamento HTTP, análise de parâmetros, autenticação, negociação de conteúdo, chamar o serviço ou objeto de editor correto, captura de exceção, formatação de resposta e retornar o código de status HTTP correto.
  • Os serviços devem chamar objetos Query e não devem armazenar o estado. Use métodos de instância, não métodos de classe. Deve haver muito poucos métodos públicos de acordo com o SRP.
  • As consultas devem ser feitas em objetos de consulta. Os métodos de objeto de consulta devem retornar um objeto, um hash ou uma matriz, não uma associação ActiveRecord.
  • Evite usar Helpers e use decoradores. Por quê? Uma armadilha comum com os auxiliares Rails é que eles podem se transformar em uma grande pilha de funções não OO, todas compartilhando um namespace e pisando umas nas outras. Mas muito pior é que não há uma boa maneira de usar qualquer tipo de polimorfismo com ajudantes Rails — fornecendo diferentes implementações para diferentes contextos ou tipos, substituindo ou subclassificando ajudantes. Eu acho que as classes auxiliares do Rails geralmente devem ser usadas para métodos utilitários, não para casos de uso específicos, como formatação de atributos de modelo para qualquer tipo de lógica de apresentação. Mantenha-os leves e arejados.
  • Evite usar preocupações e use Decoradores/Delegadores em vez disso. Por quê? Afinal, as preocupações parecem ser uma parte central do Rails e podem secar o código quando compartilhadas entre vários modelos. No entanto, o principal problema é que as preocupações não tornam o objeto do modelo mais coeso. O código é apenas melhor organizado. Em outras palavras, não há nenhuma mudança real na API do modelo.
  • Tente extrair objetos de valor de modelos para manter seu código mais limpo e agrupar atributos relacionados.
  • Sempre passe uma variável de instância por visualização.

Reestruturação

Antes de começarmos, quero discutir mais uma coisa. Quando você inicia a refatoração, geralmente acaba se perguntando: “Essa refatoração é realmente boa?”

Se você sentir que está fazendo mais separação ou isolamento entre as responsabilidades (mesmo que isso signifique adicionar mais código e novos arquivos), isso geralmente é uma coisa boa. Afinal, desacoplar um aplicativo é uma prática muito boa e facilita a realização de testes de unidade adequados.

Não vou discutir coisas, como mover a lógica de controladores para modelos, pois suponho que você já esteja fazendo isso e esteja confortável usando Rails (geralmente Skinny Controller e FAT model).

Para manter este artigo completo, não discutirei testes aqui, mas isso não significa que você não deva testar.

Pelo contrário, você deve sempre começar com um teste para garantir que as coisas estejam bem antes de seguir em frente. Isso é uma obrigação, especialmente durante a refatoração.

Então podemos implementar mudanças e garantir que todos os testes passem para as partes relevantes do código.

Extraindo objetos de valor

Primeiro, o que é um objeto de valor?

Martin Fowler explica:

Value Object é um objeto pequeno, como dinheiro ou objeto de intervalo de datas. Sua propriedade chave é que eles seguem a semântica de valor ao invés da semântica de referência.

Às vezes você pode encontrar uma situação em que um conceito merece sua própria abstração e cuja igualdade não se baseia no valor, mas na identidade. Os exemplos incluem a data, o URI e o nome do caminho do Ruby. A extração para um objeto de valor (ou modelo de domínio) é uma grande conveniência.

Porque se importar?

Uma das maiores vantagens de um objeto Value é a expressividade que eles ajudam a alcançar em seu código. Seu código tenderá a ser muito mais claro, ou pelo menos pode ser se você tiver boas práticas de nomenclatura. Como o Value Object é uma abstração, ele leva a um código mais limpo e a menos erros.

Outra grande vitória é a imutabilidade. A imutabilidade dos objetos é muito importante. Quando estamos armazenando certos conjuntos de dados, que podem ser usados ​​em um objeto de valor, geralmente não quero que esses dados sejam manipulados.

Quando isso é útil?

Não existe uma resposta única, de tamanho único. Faça o que é melhor para você e o que faz sentido em qualquer situação.

Indo além disso, porém, existem algumas diretrizes que uso para me ajudar a tomar essa decisão.

Se você pensa em um grupo de métodos relacionados, com objetos Value eles são mais expressivos. Essa expressividade significa que um objeto Value deve representar um conjunto distinto de dados, que seu desenvolvedor médio pode deduzir simplesmente observando o nome do objeto.

Como isso é feito?

Os objetos de valor devem seguir algumas regras básicas:

  • Os objetos de valor devem ter vários atributos.
  • Os atributos devem ser imutáveis ​​durante todo o ciclo de vida do objeto.
  • A igualdade é determinada pelos atributos do objeto.

Em nosso exemplo, criarei um objeto de valor EntryStatus para abstrair os atributos Entry#status_weather e Entry#status_landform para sua própria classe, que se parece com isso:

  • Amostra de essência

Nota: Este é apenas um Plain Old Ruby Object (PORO) que não herda de ActiveRecord::Base . Definimos métodos de leitura para nossos atributos e os estamos atribuindo na inicialização. Também usamos um mixin comparável para comparar objetos usando o método (<=>).

Podemos modificar o modelo de Entry para usar o objeto de valor que criamos:

  • Amostra de essência

Também podemos modificar o método EntryController#create para usar o novo objeto de valor de acordo:

  • Amostra de essência

Extrair objetos de serviço

Então, o que é um objeto Service?

O trabalho de um objeto Service é manter o código para um determinado bit de lógica de negócios. Ao contrário do estilo “modelo gordo” , onde um pequeno número de objetos contém muitos, muitos métodos para toda a lógica necessária, o uso de objetos Service resulta em muitas classes, cada uma das quais serve a um único propósito.

Por quê? Quais são os benefícios?

  • Dissociação. Os objetos de serviço ajudam você a obter mais isolamento entre os objetos.
  • Visibilidade. Objetos de serviço (se bem nomeados) mostram o que um aplicativo faz. Posso apenas dar uma olhada no diretório de serviços para ver quais recursos um aplicativo oferece.
  • Modelos e controladores de limpeza. Os controladores transformam a solicitação (parâmetros, sessão, cookies) em argumentos, passam para o serviço e redirecionam ou renderizam de acordo com a resposta do serviço. Enquanto os modelos lidam apenas com associações e persistência. A extração de código de controladores/modelos para objetos de serviço daria suporte ao SRP e tornaria o código mais desacoplado. A responsabilidade do modelo seria então apenas lidar com associações e salvar/excluir registros, enquanto o objeto de serviço teria uma responsabilidade única (SRP). Isso leva a um melhor design e melhores testes de unidade.
  • SECA e Abrace a mudança. Eu mantenho os objetos de serviço tão simples e pequenos quanto possível. Eu componho objetos de serviço com outros objetos de serviço e os reutilizo.
  • Limpe e acelere seu conjunto de testes. Os serviços são fáceis e rápidos de testar, pois são pequenos objetos Ruby com um ponto de entrada (o método de chamada). Serviços complexos são compostos com outros serviços, para que você possa dividir seus testes facilmente. Além disso, o uso de objetos de serviço facilita o mock/stub de objetos relacionados sem a necessidade de carregar todo o ambiente Rails.
  • Chamável de qualquer lugar. Objetos de serviço provavelmente serão chamados de controladores, bem como outros objetos de serviço, DelayedJob / Rescue / Sidekiq Jobs, tarefas Rake, console, etc.

Por outro lado, nada é perfeito. Uma desvantagem dos objetos Service é que eles podem ser um exagero para uma ação muito simples. Nesses casos, você pode acabar complicando, em vez de simplificar, seu código.

Quando você deve extrair objetos de serviço?

Não há nenhuma regra dura e rápida aqui também.

Normalmente, os objetos Service são melhores para sistemas de médio a grande porte; aqueles com uma quantidade razoável de lógica além das operações CRUD padrão.

Portanto, sempre que você pensar que um trecho de código pode não pertencer ao diretório onde você o adicionaria, provavelmente é uma boa ideia reconsiderar e ver se ele deve ir para um objeto de serviço.

Aqui estão alguns indicadores de quando usar objetos Service:

  • A ação é complexa.
  • A ação abrange vários modelos.
  • A ação interage com um serviço externo.
  • A ação não é uma preocupação central do modelo subjacente.
  • Existem várias maneiras de realizar a ação.

Como você deve projetar objetos de serviço?

Projetar a classe para um objeto de serviço é relativamente simples, pois você não precisa de gemas especiais, não precisa aprender uma nova DSL e pode confiar mais ou menos nas habilidades de projeto de software que já possui.

Eu costumo usar as seguintes diretrizes e convenções para projetar o objeto de serviço:

  • Não armazene o estado do objeto.
  • Use métodos de instância, não métodos de classe.
  • Deve haver muito poucos métodos públicos (de preferência um para suportar SRP.
  • Os métodos devem retornar objetos de rich result e não booleanos.
  • Os serviços ficam no diretório app/services . Eu encorajo você a usar subdiretórios para domínios com lógica de negócios pesada. Por exemplo, o arquivo app/services/report/generate_weekly.rb definirá Report::GenerateWeekly enquanto app/services/report/publish_monthly.rb definirá Report::PublishMonthly .
  • Os serviços começam com um verbo (e não terminam com Service): ApproveTransaction , SendTestNewsletter , ImportUsersFromCsv .
  • Os serviços respondem ao método de chamada. Descobri que usar outro verbo o torna um pouco redundante: ApproveTransaction.approve() não lê bem. Além disso, o método call é o método de fato para objetos lambda, procs e métodos.

Se você olhar para StatisticsController#index , você notará um grupo de métodos ( weeks_to_date_from , weeks_to_date_to , avg_distance , etc.) acoplados ao controlador. Isso não é muito bom. Considere as ramificações se você quiser gerar o relatório semanal fora statistics_controller .

No nosso caso, vamos criar Report::GenerateWeekly e extrair a lógica do relatório de StatisticsController :

  • Amostra de essência

Então StatisticsController#index agora parece mais limpo:

  • Amostra de essência

Ao aplicar o padrão de objeto Service, agrupamos o código em torno de uma ação específica e complexa e promovemos a criação de métodos menores e mais claros.

Lição de casa: considere usar o objeto Value para o WeeklyReport em vez de Struct .

Extrair objetos de consulta de controladores

O que é um objeto de consulta?

Um objeto Query é um PORO que representa uma consulta de banco de dados. Ele pode ser reutilizado em diferentes locais do aplicativo e, ao mesmo tempo, ocultar a lógica de consulta. Ele também fornece uma boa unidade isolada para teste.

Você deve extrair consultas SQL/NoSQL complexas em sua própria classe.

Cada objeto Query é responsável por retornar um conjunto de resultados baseado nos critérios/regras de negócio.

Neste exemplo, não temos consultas complexas, portanto, usar o objeto Query não será eficiente. No entanto, para fins de demonstração, vamos extrair a consulta em Report::GenerateWeekly#call e criar generate_entries_query.rb :

  • Amostra de essência

E em Report::GenerateWeekly#call , vamos substituir:

 def call @user.entries.group_by(&:week).map do |week, entries| WeeklyReport.new( ... ) end end

com:

 def call weekly_grouped_entries = GroupEntriesQuery.new(@user).call weekly_grouped_entries.map do |week, entries| WeeklyReport.new( ... ) end end

O padrão de objeto de consulta ajuda a manter sua lógica de modelo estritamente relacionada ao comportamento de uma classe, ao mesmo tempo em que mantém seus controladores magros. Uma vez que eles não são nada mais do que classes antigas do Ruby, os objetos de consulta não precisam herdar de ActiveRecord::Base e devem ser responsáveis ​​por nada mais do que executar consultas.

Extrair criar entrada para um objeto de serviço

Agora, vamos extrair a lógica de criação de uma nova entrada para um novo objeto de serviço. Vamos usar a convenção e criar CreateEntry :

  • Amostra de essência

E agora nosso EntriesController#create é o seguinte:

 def create begin CreateEntry.new(current_user, entry_params).call flash[:notice] = 'Entry was successfully created.' rescue Exception => e flash[:error] = e.message end redirect_to root_path end

Mover validações para um objeto de formulário

Agora, aqui as coisas começam a ficar mais interessantes.

Lembre-se em nossas diretrizes, concordamos que queríamos que os modelos contivessem associações e constantes, mas nada mais (sem validações e sem retornos de chamada). Então, vamos começar removendo os retornos de chamada e usar um objeto Form.

Um objeto Form é um Plain Old Ruby Object (PORO). Ele assume o controle do objeto de serviço/controlador sempre que precisar conversar com o banco de dados.

Por que usar objetos Form?

Ao tentar refatorar seu aplicativo, é sempre uma boa ideia manter o princípio de responsabilidade única (SRP) em mente.

O SRP ajuda você a tomar melhores decisões de design sobre o que uma classe deve ser responsável.

Seu modelo de tabela de banco de dados (um modelo ActiveRecord no contexto do Rails), por exemplo, representa um único registro de banco de dados no código, então não há razão para ele se preocupar com qualquer coisa que seu usuário esteja fazendo.

É aqui que entram os objetos Form.

Um objeto Form é responsável por representar um formulário em seu aplicativo. Assim, cada campo de entrada pode ser tratado como um atributo na classe. Ele pode validar que esses atributos atendem a algumas regras de validação e pode passar os dados “limpos” para onde precisa ir (por exemplo, seus modelos de banco de dados ou talvez seu construtor de consultas de pesquisa).

Quando você deve usar um objeto Form?

  • Quando você deseja extrair as validações dos modelos Rails.
  • Quando vários modelos podem ser atualizados por um único envio de formulário, convém criar um objeto Form.

Isso permite que você coloque toda a lógica do formulário (convenções de nomenclatura, validações e assim por diante) em um só lugar.

Como você cria um objeto Form?

  • Crie uma classe Ruby simples.
  • Incluir ActiveModel::Model (no Rails 3, você deve incluir Nomenclatura, Conversão e Validações)
  • Comece a usar sua nova classe de formulário como se fosse um modelo ActiveRecord regular, a maior diferença é que você não pode persistir os dados armazenados neste objeto.

Observe que você pode usar a gema de reforma, mas mantendo os POROs, criaremos entry_form.rb que se parece com isso:

  • Amostra de essência

E vamos modificar CreateEntry para começar a usar o objeto Form EntryForm :

 class CreateEntry ...... ...... def call @entry_form = ::EntryForm.new(@params) if @entry_form.valid? .... else .... end end end

Nota: Alguns de vocês diriam que não há necessidade de acessar o objeto Form a partir do objeto Service e que podemos apenas chamar o objeto Form diretamente do controller, que é um argumento válido. No entanto, eu preferiria ter um fluxo claro, e é por isso que sempre chamo o objeto Form do objeto Service.

Mover retornos de chamada para o objeto de serviço

Como concordamos anteriormente, não queremos que nossos modelos contenham validações e retornos de chamada. Extraímos as validações usando objetos Form. Mas ainda estamos usando alguns retornos de chamada ( after_create no modelo de Entry compare_speed_and_notify_user ).

Por que queremos remover callbacks dos modelos?

Os desenvolvedores Rails geralmente começam a perceber a dor de callback durante os testes. Se você não estiver testando seus modelos do ActiveRecord, começará a perceber a dor mais tarde, à medida que seu aplicativo cresce e mais lógica é necessária para chamar ou evitar o retorno de chamada.

callbacks after_* são usados ​​principalmente em relação a salvar ou persistir o objeto.

Uma vez que o objeto é salvo, a finalidade (ou seja, responsabilidade) do objeto foi cumprida. Então, se ainda vemos callbacks sendo invocados depois que o objeto foi salvo, o que provavelmente estamos vendo são callbacks que chegam fora da área de responsabilidade do objeto, e é aí que nos deparamos com problemas.

No nosso caso, estamos enviando um SMS para o usuário após salvar uma entrada, o que não está realmente relacionado ao domínio da Entrada.

Uma maneira simples de resolver o problema é mover o retorno de chamada para o objeto de serviço relacionado. Afinal, o envio de um SMS para o usuário final está relacionado ao objeto de serviço CreateEntry e não ao próprio modelo Entry.

Ao fazer isso, não precisamos mais eliminar o método compare_speed_and_notify_user em nossos testes. Tornamos simples criar uma entrada sem exigir o envio de um SMS e estamos seguindo um bom design Orientado a Objetos, garantindo que nossas classes tenham uma única responsabilidade (SRP).

Então agora nosso CreateEntry se parece com:

  • Amostra de essência

Use decoradores em vez de ajudantes

Embora possamos facilmente usar a coleção Draper de modelos de exibição e decoradores, vou me ater aos POROs para este artigo, como tenho feito até agora.

O que eu preciso é de uma classe que chame métodos no objeto decorado.

Posso usar method_missing para implementar isso, mas usarei a biblioteca padrão do Ruby SimpleDelegator .

O código a seguir mostra como usar SimpleDelegator para implementar nosso decorador base:

 % app/decorators/base_decorator.rb require 'delegate' class BaseDecorator < SimpleDelegator def initialize(base, view_context) super(base) @object = base @view_context = view_context end private def self.decorates(name) define_method(name) do @object end end def _h @view_context end end

Então, por que o método _h ?

Este método atua como um proxy para o contexto de exibição. Por padrão, o contexto de exibição é uma instância de uma classe de exibição, sendo a classe de exibição padrão ActionView::Base . Você pode acessar os auxiliares de visualização da seguinte maneira:

 _h.content_tag :div, 'my-div', class: 'my-class'

Para torná-lo mais conveniente, adicionamos um método de decorate ao ApplicationHelper :

 module ApplicationHelper # ..... def decorate(object, klass = nil) klass ||= "#{object.class}Decorator".constantize decorator = klass.new(object, self) yield decorator if block_given? decorator end # ..... end

Agora, podemos mover os auxiliares EntriesHelper para os decoradores:

 # app/decorators/entry_decorator.rb class EntryDecorator < BaseDecorator decorates :entry def readable_time_period mins = entry.time_period return Time.at(60 * mins).utc.strftime('%M <small>Mins</small>').html_safe if mins < 60 Time.at(60 * mins).utc.strftime('%H <small>Hour</small> %M <small>Mins</small>').html_safe end def readable_speed "#{sprintf('%0.2f', entry.speed)} <small>Km/H</small>".html_safe end end

E podemos usar readable_time_period e readable_speed assim:

 # app/views/entries/_entry.html.erb - <td><%= readable_speed(entry) %> </td> + <td><%= decorate(entry).readable_speed %> </td>
 - <td><%= readable_time_period(entry) %></td> + <td><%= decorate(entry).readable_time_period %></td>

Estrutura após a refatoração

Acabamos com mais arquivos, mas isso não é necessariamente uma coisa ruim (e lembre-se que, desde o início, reconhecemos que este exemplo era apenas para fins demonstrativos e não era necessariamente um bom caso de uso para refatoração):

 app ├── assets │ └── ... ├── controllers │ ├── application_controller.rb │ ├── entries_controller.rb │ └── statistics_controller.rb ├── decorators │ ├── base_decorator.rb │ └── entry_decorator.rb ├── forms │ └── entry_form.rb ├── helpers │ └── application_helper.rb ├── mailers ├── models │ ├── entry.rb │ ├── entry_status.rb │ └── user.rb ├── queries │ └── group_entries_query.rb ├── services │ ├── create_entry.rb │ └── report │ └── generate_weekly.rb └── views ├── devise │ └── .. ├── entries │ ├── _entry.html.erb │ ├── _form.html.erb │ └── index.html.erb ├── layouts │ └── application.html.erb └── statistics └── index.html.erb

Conclusão

Embora tenhamos focado no Rails neste post do blog, RoR não é uma dependência dos objetos de serviço descritos e outros POROs. Você pode usar essa abordagem com qualquer estrutura da Web, dispositivo móvel ou aplicativo de console.

Ao usar o MVC como a arquitetura de aplicativos da Web, tudo permanece acoplado e faz com que você fique mais lento porque a maioria das alterações tem impacto em outras partes do aplicativo. Além disso, isso força você a pensar onde colocar alguma lógica de negócios – ela deve entrar no modelo, no controlador ou na visualização?

Usando POROs simples, movemos a lógica de negócios para modelos ou serviços que não herdam do ActiveRecord , o que já é uma grande vitória, sem contar que temos um código mais limpo, que suporta SRP e testes unitários mais rápidos.

A arquitetura limpa visa colocar os casos de uso no centro/topo de sua estrutura, para que você possa ver facilmente o que seu aplicativo faz. Também facilita a adoção de mudanças, pois é muito mais modular e isolado.

Espero ter demonstrado como o uso de Plain Old Ruby Objects e mais abstrações dissocia preocupações, simplifica o teste e ajuda a produzir um código limpo e de fácil manutenção.

Relacionado: Quais são os benefícios do Ruby on Rails? Após duas décadas de programação, uso Rails