Um guia para mecanismos Rails na natureza: exemplos do mundo real de mecanismos Rails em ação
Publicados: 2022-03-11Por que os Rails Engines não são usados com mais frequência? Não sei a resposta, mas acho que a generalização de “Tudo é um Motor” escondeu os domínios do problema que eles podem ajudar a resolver.
A excelente documentação do Rails Guide para começar com os Rails Engines faz referência a quatro exemplos populares de implementações do Rails Engine: Forem, Devise, Spree e RefineryCMS. Estes são fantásticos casos de uso do mundo real para Engines, cada um usando uma abordagem diferente para integração com um aplicativo Rails.
Examinar partes de como essas gems são configuradas e compostas dará aos desenvolvedores avançados de Ruby on Rails um conhecimento valioso de quais padrões ou técnicas são experimentadas e testadas na natureza, então quando chegar a hora você poderá ter algumas opções extras para avaliar.
Eu espero que você tenha uma familiaridade superficial de como um Engine funciona, então se você sentir que algo não está dando certo, por favor leia o mais excelente Guia do Rails Getting Started With Engines .
Sem mais delongas, vamos nos aventurar no mundo selvagem dos exemplos de motores Rails!
Formulário
Um motor para Rails que pretende ser o melhor pequeno sistema de fórum de todos os tempos
Esta jóia segue a direção do Rails Guide on Engines ao pé da letra. É um exemplo considerável e examinar seu repositório lhe dará uma idéia de até onde você pode esticar a configuração básica.
É uma gem de mecanismo único que usa algumas técnicas para se integrar ao aplicativo principal.
module ::Forem class Engine < Rails::Engine isolate_namespace Forem # ... config.to_prepare do Decorators.register! Engine.root, Rails.root end # ... end end
A parte interessante aqui é o Decorators.register!
método de classe, exposto pela gem Decorators. Ele encapsula arquivos de carregamento que não seriam incluídos no processo de carregamento automático do Rails. Você deve se lembrar que usar declarações require
explícitas estraga o recarregamento automático no modo de desenvolvimento, então isso é um salva-vidas! Será mais claro usar o exemplo do Guia para ilustrar o que está acontecendo:
config.to_prepare do Dir.glob(Rails.root + "app/decorators/**/*_decorator*.rb").each do |c| require_dependency(c) end end
A maior parte da mágica para a configuração do Forem acontece na definição do módulo principal superior do Forem
. Este arquivo depende de uma variável user_class
sendo definida em um arquivo inicializador:
Forem.user_class = "User"
Você faz isso usando mattr_accessor
, mas está tudo no Rails Guide, então não vou repetir isso aqui. Com isso, o Forem decora a classe de usuário com tudo o que precisa para executar seu aplicativo:
module Forem class << self def decorate_user_class! Forem.user_class.class_eval do extend Forem::Autocomplete include Forem::DefaultPermissions has_many :forem_posts, :class_name => "Forem::Post", :foreign_key => "user_id" # ... def forem_moderate_posts? Forem.moderate_first_post && !forem_approved_to_post? end alias_method :forem_needs_moderation?, :forem_moderate_posts? # ...
O que acaba sendo bastante! Eu cortei a maioria, mas deixei uma definição de associação, bem como um método de instância para mostrar o tipo de linhas que você pode encontrar lá.
Vislumbrar o arquivo inteiro pode mostrar como pode ser gerenciável a portabilidade de parte do seu aplicativo para reutilização em um mecanismo.
Decoração é o nome do jogo no uso padrão do Engine. Como usuário final da gem, você pode substituir o modelo, a visualização e os controladores criando suas próprias versões das classes usando o caminho do arquivo e as convenções de nomenclatura de arquivo apresentadas no README da gem do decorador. No entanto, há um custo associado a essa abordagem, especialmente quando o Engine recebe uma atualização de versão principal – a manutenção de manter suas decorações funcionando pode rapidamente sair do controle. Não estou citando o Forem aqui, acredito que eles são firmes em manter uma funcionalidade central bem unida, mas lembre-se disso se você criar um mecanismo e decidir fazer uma revisão.
Vamos recapitular este: este é o padrão de design do mecanismo Rails que depende de usuários finais decorando visualizações, controladores e modelos, além de definir configurações básicas por meio de arquivos de inicialização. Isso funciona bem para funcionalidades muito focadas e relacionadas.
Conceber
Uma solução de autenticação flexível para Rails
Você verá que um Engine é muito semelhante a uma aplicação Rails, com diretórios de views
, controllers
e models
. O Devise é um bom exemplo de encapsulamento de um aplicativo e exposição de um ponto de integração conveniente. Vamos ver como exatamente isso funciona.
Você reconhecerá estas linhas de código se for um desenvolvedor Rails por mais de algumas semanas:
class User < ActiveRecord::Base # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable end
Cada parâmetro passado para o método devise
representa um módulo dentro do Devise Engine. Há dez desses módulos ao todo que herdam do familiar ActiveSupport::Concern
. Eles estendem sua classe User
invocando o método devise
dentro de seu escopo.
Ter esse tipo de ponto de integração é muito flexível, você pode adicionar ou remover qualquer um desses parâmetros para alterar o nível de funcionalidade que você deseja que o Engine execute. Isso também significa que você não precisa codificar qual modelo você gostaria de usar dentro de um arquivo inicializador, como sugerido pelo Rails Guide on Engines. Em outras palavras, isso não é necessário:
Devise.user_model = 'User'
Essa abstração também significa que você pode aplicar isso a mais de um modelo de usuário dentro do mesmo aplicativo ( admin
e user
, por exemplo), enquanto a abordagem do arquivo de configuração o deixaria vinculado a um único modelo com autenticação. Este não é o maior ponto de venda, mas ilustra uma maneira diferente de resolver um problema.
Devise estende ActiveRecord::Base
com seu próprio módulo que inclui a definição do método devise
:
# lib/devise/orm/active_record.rb ActiveRecord::Base.extend Devise::Models
Qualquer classe herdada de ActiveRecord::Base
agora terá acesso aos métodos de classe definidos em Devise::Models
:
#lib/devise/models.rb module Devise module Models # ... def devise(*modules) selected_modules = modules.map(&:to_sym).uniq # ... selected_modules.each do |m| mod = Devise::Models.const_get(m.to_s.classify) if mod.const_defined?("ClassMethods") class_mod = mod.const_get("ClassMethods") extend class_mod # ... end include mod end end # ... end end
(Removi muito código ( # ...
) para destacar as partes importantes. )
Parafraseando o código, para cada nome de módulo passado para o método devise
temos:
- carregando o módulo que especificamos que vive em
Devise::Models
(Devise::Models.const_get(m.to_s.classify
) - estendendo a classe
User
com o móduloClassMethods
se tiver um - inclua o módulo especificado (
include mod
) para adicionar seus métodos de instância à classe que chama o métododevise
(User
)
Se você quisesse criar um módulo que pudesse ser carregado dessa maneira, precisaria garantir que ele seguisse a interface ActiveSupport::Concern
usual, mas o namespace sob Devise:Models
, pois é onde procuramos recuperar a constante:
module Devise module Models module Authenticatable extend ActiveSupport::Concern included do # ... end module ClassMethods # ... end end end end
Ufa.
Se você já usou o Rails' Concerns antes e experimentou a reutilização que eles oferecem, então você pode apreciar as sutilezas desta abordagem. Resumindo, dividir a funcionalidade dessa maneira facilita o teste ao ser abstraído de um modelo ActiveRecord
e tem uma sobrecarga menor do que o padrão padrão usado pelo Forem quando se trata de estender a funcionalidade.
Este padrão consiste em dividir sua funcionalidade em Rails Concerns e expor um ponto de configuração para incluí-los ou excluí-los dentro de um determinado escopo. Um Engine formado dessa maneira é conveniente para o usuário final – um fator que contribui para o sucesso e a popularidade do Devise. E agora você também sabe como fazer!
Farra
Uma solução completa de e-commerce de código aberto para Ruby on Rails
Spree passou por um esforço colossal para controlar sua aplicação monolítica com uma mudança para o uso de Engines. O projeto de arquitetura com o qual eles estão agora rolando é uma gema “Spree” que contém muitas gemas de mecanismo.
Esses mecanismos criam partições no comportamento que você pode estar acostumado a ver em um aplicativo monolítico ou espalhado por aplicativos:
- spree_api (API RESTful)
- spree_frontend (componentes voltados para o usuário)
- spree_backend (área de administração)
- spree_cmd (ferramentas de linha de comando)
- spree_core (Models & Mailers, os componentes básicos do Spree que não podem ser executados sem)
- spree_sample (dados de amostra)
A gema abrangente os une, deixando o desenvolvedor com uma escolha no nível de funcionalidade a ser exigido. Por exemplo, você pode executar apenas com o mecanismo spree_core
e envolver sua própria interface em torno dele.
A gem principal do Spree requer esses mecanismos:
# lib/spree.rb require 'spree_core' require 'spree_api' require 'spree_backend' require 'spree_frontend'
Cada Engine precisa então customizar seu engine_name
e caminho root
(o último geralmente apontando para a gem de nível superior) e se configurar conectando-se ao processo de inicialização:
# api/lib/spree/api/engine.rb require 'rails/engine' module Spree module Api class Engine < Rails::Engine isolate_namespace Spree engine_name 'spree_api' def self.root @root ||= Pathname.new(File.expand_path('../../../../', __FILE__)) end initializer "spree.environment", :before => :load_config_initializers do |app| app.config.spree = Spree::Core::Environment.new end # ... end end end
Você pode ou não reconhecer este método inicializador: ele faz parte do Railtie
e é um gancho que lhe dá a oportunidade de adicionar ou remover etapas da inicialização do framework Rails. O Spree depende muito desse gancho para configurar seu ambiente complexo para todos os seus mecanismos.

Usando o exemplo acima em tempo de execução, você terá acesso às suas configurações acessando a constante Rails
de nível superior:
Rails.application.config.spree
Com este guia de padrões de design do mecanismo Rails acima, poderíamos encerrar o dia, mas o Spree tem uma tonelada de código incrível, então vamos mergulhar em como eles utilizam a inicialização para compartilhar a configuração entre os mecanismos e o aplicativo Rails principal.
O Spree tem um sistema de preferências complexo que carrega adicionando uma etapa no processo de inicialização:
# api/lib/spree/api/engine.rb initializer "spree.environment", :before => :load_config_initializers do |app| app.config.spree = Spree::Core::Environment.new end
Aqui, estamos anexando ao app.config.spree
uma nova instância Spree::Core::Environment
. Dentro da aplicação Rails você poderá acessar via Rails.application.config.spree
de qualquer lugar - models, controllers, views.
Continuando, a classe Spree::Core::Environment
que criamos se parece com isso:
module Spree module Core class Environment attr_accessor :preferences def initialize @preferences = Spree::AppConfiguration.new end end end end
Ele expõe uma variável :preferences
definida para uma nova instância da classe Spree::AppConfiguration
, que por sua vez usa um método de preference
definido na classe Preferences::Configuration
para definir opções com padrões para a configuração geral do aplicativo:
module Spree class AppConfiguration < Preferences::Configuration # Alphabetized to more easily lookup particular preferences preference :address_requires_state, :boolean, default: true # should state/state_name be required preference :admin_interface_logo, :string, default: 'logo/spree_50.png' preference :admin_products_per_page, :integer, default: 10 preference :allow_checkout_on_gateway_error, :boolean, default: false # ... end end
Eu não vou mostrar o arquivo Preferences::Configuration
porque vai demorar um pouco para explicar, mas essencialmente é açúcar sintático para obter e definir preferências. (Na verdade, isso é uma simplificação excessiva de sua funcionalidade, pois o sistema de preferências salvará valores diferentes do padrão para preferências existentes ou novas no banco de dados, para qualquer classe ActiveRecord
com uma coluna :preference
- mas você não precisa saiba disso.)
Aqui está uma dessas opções em ação:
module Spree class Calculator < Spree::Base def self.calculators Rails.application.config.spree.calculators end # ... end end
As calculadoras controlam todos os tipos de coisas no Spree – custos de envio, promoções, ajustes de preços de produtos – portanto, ter um mecanismo para trocá-los dessa maneira aumenta a extensibilidade do mecanismo.
Uma das muitas maneiras de substituir as configurações padrão para essas preferências é dentro de um inicializador no aplicativo Rails principal:
# config/initializergs/spree.rb Spree::Config do |config| config.admin_interface_logo = 'company_logo.png' end
Se você leu o RailsGuide on Engines, considerou seus padrões de design ou construiu um Engine você mesmo, saberá que é trivial expor um setter em um arquivo inicializador para alguém usar. Então você pode estar se perguntando, por que todo o barulho com o sistema de configuração e preferência? Lembre-se, o sistema de preferência resolve um problema de domínio para Spree. Conectar-se ao processo de inicialização e obter acesso ao framework Rails pode ajudá-lo a atender seus requisitos de maneira sustentável.
Esse padrão de design de mecanismo se concentra no uso da estrutura Rails como a constante entre suas muitas partes móveis para armazenar configurações que (geralmente) não mudam em tempo de execução, mas mudam entre as instalações do aplicativo.
Se você já tentou colocar um whitelabel em uma aplicação Rails, você pode estar familiarizado com este cenário de preferências, e já sentiu a dor de tabelas complicadas de “configurações” de banco de dados dentro de um longo processo de configuração para cada nova aplicação. Agora você sabe que um caminho diferente está disponível e isso é incrível - mais cinco!
RefinariaCMS
Um sistema de gerenciamento de conteúdo de código aberto para Rails
Convenção sobre configuração de alguém? Os Rails Engines podem definitivamente parecer mais como um exercício de configuração às vezes, mas o RefineryCMS lembra um pouco dessa mágica do Rails. Este é todo o conteúdo do diretório lib
:
# lib/refinerycms.rb require 'refinery/all' # lib/refinery/all.rb %w(core authentication dashboard images resources pages).each do |extension| require "refinerycms-#{extension}" end
Uau. Se você não pode dizer por isso, a equipe da Refinaria realmente sabe o que está fazendo. Eles rolam com o conceito de uma extension
que é em essência outro Engine. Como o Spree, ele possui uma jóia de costura abrangente, mas usa apenas dois pontos e reúne uma coleção de mecanismos para fornecer seu conjunto completo de funcionalidades.
Extensões também são criadas pelos usuários do Engine, para criar seu próprio mash-up de recursos do CMS para blogs, notícias, portfólios, depoimentos, consultas, etc. (é uma longa lista), todos conectados ao núcleo RefineryCMS.
Esse design pode chamar sua atenção por sua abordagem modular, e o Refinery é um ótimo exemplo desse padrão de design do Rails. "Como funciona?" Eu ouço você perguntar.
O mecanismo core
mapeia alguns ganchos para os outros mecanismos usarem:
# core/lib/refinery/engine.rb module Refinery module Engine def after_inclusion(&block) if block && block.respond_to?(:call) after_inclusion_procs << block else raise 'Anything added to be called after_inclusion must be callable (respond to #call).' end end def before_inclusion(&block) if block && block.respond_to?(:call) before_inclusion_procs << block else raise 'Anything added to be called before_inclusion must be callable (respond to #call).' end end private def after_inclusion_procs @@after_inclusion_procs ||= [] end def before_inclusion_procs @@before_inclusion_procs ||= [] end end end
Como você pode ver o before_inclusion
e after_inclusion
apenas armazenam uma lista de procs que serão executados posteriormente. O processo de inclusão do Refinery estende os aplicativos Rails atualmente carregados com os controladores e auxiliares do Refinery. Aqui está um em ação:
# authentication/lib/refinery/authentication/engine.rb before_inclusion do [Refinery::AdminController, ::ApplicationController].each do |c| Refinery.include_once(c, Refinery::AuthenticatedSystem) end end
Tenho certeza de que você colocou métodos de autenticação em seu ApplicationController
e AdminController
antes, esta é uma maneira programática de fazer isso.
Observar o restante desse arquivo do Authentication Engine nos ajudará a obter alguns outros componentes importantes:
module Refinery module Authentication class Engine < ::Rails::Engine extend Refinery::Engine isolate_namespace Refinery engine_name :refinery_authentication config.autoload_paths += %W( #{config.root}/lib ) initializer "register refinery_user plugin" do Refinery::Plugin.register do |plugin| plugin.pathname = root plugin.name = 'refinery_users' plugin.menu_match = %r{refinery/users$} plugin.url = proc { Refinery::Core::Engine.routes.url_helpers.admin_users_path } end end end config.after_initialize do Refinery.register_extension(Refinery::Authentication) end # ... end end
Sob o capô, as extensões do Refinery usam um sistema Plugin
. A etapa do initializer
parecerá familiar a partir da análise de código do Spree, aqui estão apenas atendendo aos requisitos de métodos de register
a serem adicionados à lista de Refinery::Plugins
que a extensão do core
acompanha, e o Refinery.register_extension
apenas adiciona o nome do módulo para uma lista armazenada em um acessador de classe.
Aqui está um choque: a classe Refinery::Authentication
é realmente um wrapper em torno do Devise, com alguma personalização. Então são tartarugas até lá embaixo!
As extensões e plugins são conceitos que a Refinery desenvolveu para dar suporte ao seu rico ecossistema de aplicativos e ferramentas de mini-rails - pense rake generate refinery:engine
. O padrão de design aqui difere do Spree ao impor uma API adicional em torno do Rails Engine para auxiliar no gerenciamento de sua composição.
O idioma “The Rails Way” está no cerne da Refinaria, cada vez mais presente em seus aplicativos de mini-rails, mas de fora você não saberia disso. Projetar limites no nível de composição do aplicativo é tão importante, possivelmente mais, do que criar uma API limpa para suas classes e módulos usados em seus aplicativos Rails.
O encapsulamento de código sobre o qual você não tem controle direto é um padrão comum, é uma previsão na redução do tempo de manutenção para quando esse código for alterado, limitando o número de locais que você precisará fazer alterações para oferecer suporte a atualizações. Aplicar essa técnica junto com a funcionalidade de particionamento cria uma plataforma flexível para composição, e aqui está um exemplo do mundo real bem debaixo do seu nariz - tenho que amar o código aberto!
Conclusão
Vimos quatro abordagens para projetar padrões de mecanismo Rails analisando gems populares sendo usadas em aplicativos do mundo real. Vale a pena ler seus repositórios para aprender com uma vasta experiência já aplicada e iterada. Fique de pé sobre os ombros dos gigantes.
Neste guia Rails, focamos nos padrões de projeto e técnicas para integrar os Rails Engines e os aplicativos Rails de seus usuários finais, para que você possa adicionar o conhecimento deles ao seu cinto de ferramentas Rails.
Espero que você tenha aprendido tanto quanto eu revisando este código e se sinta inspirado a dar uma chance aos Rails Engines quando eles se encaixarem. Um enorme obrigado aos mantenedores e colaboradores das gemas que analisamos. Ótimo trabalho gente!