Una guía para los motores Rails en la naturaleza: Ejemplos reales de motores Rails en acción

Publicado: 2022-03-11

¿Por qué Rails Engines no se usa con más frecuencia? No sé la respuesta, pero creo que la generalización de "Todo es un motor" ha ocultado los dominios del problema que pueden ayudar a resolver.

La excelente documentación de Rails Guide para comenzar con Rails Engines hace referencia a cuatro ejemplos populares de implementaciones de Rails Engine: Forem, Devise, Spree y RefineryCMS. Estos son fantásticos casos de uso del mundo real para Engines, cada uno de los cuales utiliza un enfoque diferente para integrarse con una aplicación Rails.

Cada guía Rails debe cubrir el tema de los patrones de diseño de los motores Rails y sus ejemplos.

Examinar partes de cómo se configuran y componen estas gemas les dará a los desarrolladores avanzados de Ruby on Rails un conocimiento valioso de qué patrones o técnicas se prueban y prueban en la naturaleza, por lo que cuando llegue el momento, puede tener algunas opciones adicionales para evaluar.

Espero que tenga una familiaridad superficial con el funcionamiento de un motor, así que si siente que algo no cuadra, lea detenidamente la excelente Guía de Rails Introducción a los motores .

Sin más preámbulos, aventurémonos en el salvaje mundo de los ejemplos de motores de Rails.

forem

Un motor para Rails que aspira a ser el mejor sistema de pequeños foros de todos los tiempos.

Esta joya sigue al pie de la letra la dirección de Rails Guide on Engines. Es un ejemplo considerable y examinar su repositorio le dará una idea de hasta dónde puede ampliar la configuración básica.

Es una joya de un solo motor que utiliza un par de técnicas para integrarse con la aplicación principal.

 module ::Forem class Engine < Rails::Engine isolate_namespace Forem # ... config.to_prepare do Decorators.register! Engine.root, Rails.root end # ... end end

¡La parte interesante aquí es Decorators.register! método de clase, expuesto por la gema Decorators. Encapsula la carga de archivos que no se incluirían en el proceso de carga automática de Rails. Puede recordar que el uso de declaraciones de require explícitas arruina la recarga automática en el modo de desarrollo, ¡así que esto es un salvavidas! Será más claro usar el ejemplo de la Guía para ilustrar lo que está sucediendo:

 config.to_prepare do Dir.glob(Rails.root + "app/decorators/**/*_decorator*.rb").each do |c| require_dependency(c) end end

La mayor parte de la magia para la configuración de Forem ocurre en la definición del módulo principal superior de Forem . Este archivo se basa en una variable de user_class de usuario que se establece en un archivo inicializador:

 Forem.user_class = "User"

Puedes lograr esto usando mattr_accessor pero todo está en la Guía de Rails, así que no lo repetiré aquí. Con esto en su lugar, Forem decora la clase de usuario con todo lo que necesita para ejecutar su aplicación:

 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? # ...

¡Lo cual resulta ser bastante! Recorté la mayoría, pero dejé una definición de asociación, así como un método de instancia para mostrarle el tipo de líneas que puede encontrar allí.

Echar un vistazo a todo el archivo puede mostrarle lo manejable que puede ser la portabilidad de parte de su aplicación para su reutilización en un motor.

Decorar es el nombre del juego en el uso predeterminado del motor. Como usuario final de la gema, puede anular el modelo, la vista y los controladores mediante la creación de sus propias versiones de las clases mediante la ruta de archivo y las convenciones de nomenclatura de archivos establecidas en el LÉAME de la gema decoradora. Sin embargo, hay un costo asociado con este enfoque, especialmente cuando el motor obtiene una actualización de versión principal: el mantenimiento de las decoraciones en funcionamiento puede salirse rápidamente de control. No estoy citando a Forem aquí, creo que son firmes en mantener una funcionalidad central muy unida, pero tenga esto en cuenta si crea un motor y decide realizar una revisión.

Recapitulemos esto: este es el patrón de diseño del motor de Rails predeterminado que se basa en que los usuarios finales decoren vistas, controladores y modelos, además de configurar ajustes básicos a través de archivos de inicialización. Esto funciona bien para una funcionalidad muy enfocada y relacionada.

Idear

Una solución de autenticación flexible para Rails

Encontrará que un motor es muy similar a una aplicación Rails, con views , controllers y directorios de models . Devise es un buen ejemplo de encapsular una aplicación y exponer un punto de integración conveniente. Veamos cómo funciona exactamente eso.

Reconocerá estas líneas de código si ha sido desarrollador de Rails durante más de unas pocas 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 pasado al método de devise representa un módulo dentro del motor de diseño. Hay diez de estos módulos en total que heredan del familiar ActiveSupport::Concern . Estos amplían su clase de User al invocar el método de devise dentro de su alcance.

Tener este tipo de punto de integración es muy flexible, puede agregar o eliminar cualquiera de estos parámetros para cambiar el nivel de funcionalidad que necesita que realice el motor. También significa que no necesita codificar qué modelo le gustaría usar dentro de un archivo de inicialización, como lo sugiere Rails Guide on Engines. En otras palabras, esto no es necesario:

 Devise.user_model = 'User'

Esta abstracción también significa que puede aplicar esto a más de un modelo de usuario dentro de la misma aplicación ( admin y user , por ejemplo), mientras que el enfoque del archivo de configuración lo dejaría atado a un solo modelo con autenticación. Este no es el punto de venta más importante, pero ilustra una forma diferente de resolver un problema.

Devise amplía ActiveRecord::Base con su propio módulo que incluye la definición del método devise :

 # lib/devise/orm/active_record.rb ActiveRecord::Base.extend Devise::Models

Cualquier clase que herede de ActiveRecord::Base ahora tendrá acceso a los métodos de clase definidos en 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

(He eliminado una gran cantidad de código ( # ... ) para resaltar las partes importantes).

Parafraseando el código, para cada nombre de módulo pasado al método del devise estamos:

  • cargando el módulo que especificamos que vive en Devise::Models ( Devise::Models.const_get(m.to_s.classify )
  • extendiendo la clase User con el módulo ClassMethods si tiene uno
  • incluir el módulo especificado ( include mod ) para agregar sus métodos de instancia a la clase que llama al método de devise ( User )

Si desea crear un módulo que pueda cargarse de esta manera, debe asegurarse de que siga la interfaz habitual ActiveSupport::Concern , pero coloque un espacio de nombre en Devise:Models , ya que aquí es donde buscamos recuperar la constante:

 module Devise module Models module Authenticatable extend ActiveSupport::Concern included do # ... end module ClassMethods # ... end end end end

Uf.

Si ha usado Rails' Concerns anteriormente y ha experimentado la reutilización que ofrecen, entonces puede apreciar las sutilezas de este enfoque. En resumen, dividir la funcionalidad de esta manera facilita las pruebas al abstraerse de un modelo ActiveRecord y tiene una sobrecarga menor que el patrón predeterminado utilizado por Forem cuando se trata de ampliar la funcionalidad.

Este patrón consiste en dividir su funcionalidad en Rails Concerns y exponer un punto de configuración para incluirlos o excluirlos dentro de un ámbito determinado. Un motor formado de esta manera es conveniente para el usuario final, un factor que contribuye al éxito y la popularidad de Devise. ¡Y ahora tú también sabes cómo hacerlo!

Juerga

Una completa solución de comercio electrónico de código abierto para Ruby on Rails

Spree realizó un esfuerzo colosal para controlar su aplicación monolítica con el paso al uso de Engines. El diseño de arquitectura con el que ahora están rodando es una joya "Spree" que contiene muchas gemas de motor.

Estos motores crean particiones en el comportamiento que puede estar acostumbrado a ver dentro de una aplicación monolítica o repartidas entre aplicaciones:

  • spree_api (API RESTful)
  • spree_frontend (componentes orientados al usuario)
  • spree_backend (área de administración)
  • spree_cmd (herramientas de línea de comandos)
  • spree_core (Modelos y anuncios publicitarios, los componentes básicos de Spree sin los que no puede funcionar)
  • spree_sample (Datos de muestra)

La gema circundante los une, lo que deja al desarrollador con una opción en el nivel de funcionalidad que requiere. Por ejemplo, puede ejecutar solo el motor spree_core y envolverlo con su propia interfaz.

La joya principal de Spree requiere estos motores:

 # lib/spree.rb require 'spree_core' require 'spree_api' require 'spree_backend' require 'spree_frontend'

Luego, cada motor debe personalizar su engine_name y la ruta root (este último generalmente apunta a la gema de nivel superior) y configurarse a sí mismos al conectarse al proceso de inicialización:

 # 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

Puede reconocer o no este método de inicialización: es parte de Railtie y es un gancho que le brinda la oportunidad de agregar o eliminar pasos de la inicialización del marco Rails. Spree se basa en gran medida en este gancho para configurar su entorno complejo para todos sus motores.

Usando el ejemplo anterior en tiempo de ejecución, tendrá acceso a su configuración accediendo a la constante de Rails de nivel superior:

 Rails.application.config.spree

Con esta guía de patrón de diseño del motor de Rails anterior, podríamos darlo por terminado, pero Spree tiene un montón de código increíble, así que profundicemos en cómo utilizan la inicialización para compartir la configuración entre los motores y la aplicación principal de Rails.

Spree tiene un sistema de preferencias complejo que carga al agregar un paso en el proceso de inicialización:

 # api/lib/spree/api/engine.rb initializer "spree.environment", :before => :load_config_initializers do |app| app.config.spree = Spree::Core::Environment.new end

Aquí, adjuntamos a app.config.spree una nueva instancia de Spree::Core::Environment . Dentro de la aplicación Rails, podrá acceder a ella a través de Rails.application.config.spree desde cualquier lugar: modelos, controladores, vistas.

Avanzando hacia abajo, la clase Spree::Core::Environment que creamos se ve así:

 module Spree module Core class Environment attr_accessor :preferences def initialize @preferences = Spree::AppConfiguration.new end end end end

Expone una variable :preferences establecida en una nueva instancia de la clase Spree::AppConfiguration , que a su vez usa un método de preference definido en la clase Preferences::Configuration para establecer opciones con valores predeterminados para la configuración general de la aplicación:

 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

No mostraré el archivo Preferences::Configuration porque tomará un poco de explicación, pero esencialmente es azúcar sintáctico para obtener y configurar preferencias. (En verdad, esto es una simplificación excesiva de su funcionalidad, ya que el sistema de preferencias guardará valores distintos a los predeterminados para las preferencias existentes o nuevas en la base de datos, para cualquier clase de ActiveRecord con una columna :preference , pero no es necesario que saber que.)

Aquí está una de esas opciones en acción:

 module Spree class Calculator < Spree::Base def self.calculators Rails.application.config.spree.calculators end # ... end end

Las calculadoras controlan todo tipo de cosas en Spree: costos de envío, promociones, ajustes de precios de productos, por lo que tener un mecanismo para intercambiarlos de esta manera aumenta la extensibilidad del motor.

Una de las muchas formas en que puede anular la configuración predeterminada para estas preferencias es dentro de un inicializador en la aplicación Rails principal:

 # config/initializergs/spree.rb Spree::Config do |config| config.admin_interface_logo = 'company_logo.png' end

Si leyó RailsGuide sobre motores, consideró sus patrones de diseño o construyó un motor usted mismo, sabrá que es trivial exponer un setter en un archivo de inicialización para que alguien lo use. Quizás se esté preguntando, ¿por qué tanto alboroto con la configuración y el sistema de preferencias? Recuerde, el sistema de preferencias resuelve un problema de dominio para Spree. Conectarse al proceso de inicialización y obtener acceso al marco de Rails podría ayudarlo a cumplir con sus requisitos de manera sostenible.

Este patrón de diseño de motor se enfoca en usar el marco Rails como la constante entre sus muchas partes móviles para almacenar configuraciones que (generalmente) no cambian en tiempo de ejecución, pero sí cambian entre instalaciones de aplicaciones.

Si alguna vez ha intentado etiquetar en blanco una aplicación Rails, es posible que esté familiarizado con este escenario de preferencias y haya sentido el dolor de las intrincadas tablas de "configuración" de la base de datos dentro de un largo proceso de configuración para cada nueva aplicación. Ahora sabes que hay un camino diferente disponible y eso es increíble: ¡choca los cinco!

RefineríaCMS

Un sistema de gestión de contenido de código abierto para Rails

Convención sobre la configuración de alguien? Rails Engines definitivamente puede parecer más un ejercicio de configuración a veces, pero RefineryCMS recuerda algo de esa magia de Rails. Este es el contenido completo de su directorio 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

Guau. Si no puede darse cuenta con esto, el equipo de Refinery realmente sabe lo que está haciendo. Ruedan con el concepto de una extension que es, en esencia, otro motor. Al igual que Spree, tiene una gema de costura envolvente, pero solo usa dos puntadas y reúne una colección de motores para ofrecer su conjunto completo de funciones.

Los usuarios de Engine también crean extensiones para crear su propia combinación de funciones de CMS para blogs, noticias, carteras, testimonios, consultas, etc. (es una lista larga), todas conectadas al núcleo de RefineryCMS.

Este diseño puede llamar su atención por su enfoque modular, y Refinery es un gran ejemplo de este patrón de diseño de Rails. "¿Como funciona?" Te escucho preguntar.

El motor core asigna algunos ganchos para que los usen los otros motores:

 # 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 puede ver, before_inclusion y after_inclusion solo almacenan una lista de procesos que se ejecutarán más adelante. El proceso de inclusión de Refinery amplía las aplicaciones de Rails actualmente cargadas con los controladores y ayudantes de Refinery. Aquí hay uno en acción:

 # authentication/lib/refinery/authentication/engine.rb before_inclusion do [Refinery::AdminController, ::ApplicationController].each do |c| Refinery.include_once(c, Refinery::AuthenticatedSystem) end end

Estoy seguro de que ha puesto métodos de autenticación en su ApplicationController y AdminController antes, esta es una forma programática de hacerlo.

Mirar el resto de ese archivo del motor de autenticación nos ayudará a obtener algunos otros componentes clave:

 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

Debajo del capó, las extensiones de refinería usan un sistema de Plugin . El paso del initializer parecerá familiar por el análisis del código de Spree, aquí solo cumplimos con los requisitos de los métodos de register para agregarlos a la lista de Refinery::Plugins de los que realiza un seguimiento la extensión core , y Refinery.register_extension solo agrega el nombre del módulo a una lista almacenada en una clase de acceso.

Aquí hay una sorpresa: la clase Refinery::Authentication es realmente un envoltorio alrededor de Devise, con algunas personalizaciones. ¡Así que son tortugas hasta el fondo!

Las extensiones y complementos son conceptos que Refinery ha desarrollado para respaldar su rico ecosistema de aplicaciones y herramientas de mini-rieles: piense en rake generate refinery:engine . El patrón de diseño aquí difiere de Spree al imponer una API adicional alrededor de Rails Engine para ayudar a administrar su composición.

El idioma "The Rails Way" está en el centro de Refinery, cada vez más presente en sus aplicaciones de mini-rieles, pero desde afuera no lo sabrías. Diseñar límites en el nivel de composición de la aplicación es tan importante, posiblemente más, que crear una API limpia para sus Clases y Módulos utilizados dentro de sus Aplicaciones Rails.

Envolver el código sobre el que no tiene control directo es un patrón común, es una previsión para reducir el tiempo de mantenimiento para cuando ese código cambie, limitando la cantidad de lugares que necesitará para hacer modificaciones para admitir actualizaciones. La aplicación de esta técnica junto con la funcionalidad de partición crea una plataforma flexible para la composición, y aquí hay un ejemplo del mundo real sentado justo debajo de sus narices: ¡me encanta el código abierto!

Conclusión


Hemos visto cuatro enfoques para diseñar patrones de motor de Rails mediante el análisis de gemas populares que se utilizan en aplicaciones del mundo real. Vale la pena leer sus repositorios para aprender de una gran cantidad de experiencia ya aplicada y repetida. Pararse en los hombros de los gigantes.

En esta guía de Rails, nos hemos centrado en los patrones y técnicas de diseño para integrar Rails Engines y las aplicaciones Rails de sus usuarios finales, de modo que pueda agregar el conocimiento de estos a su cinturón de herramientas de Rails.

Espero que haya aprendido tanto como yo al revisar este código y se sienta inspirado para darle una oportunidad a Rails Engines cuando cumplan con los requisitos. Muchas gracias a los mantenedores y colaboradores de las gemas que revisamos. ¡Buen trabajo gente!

Relacionado: Truncamiento de marca de tiempo: un cuento de ActiveRecord de Ruby on Rails