Un guide des moteurs ferroviaires dans la nature : exemples réels de moteurs ferroviaires en action

Publié: 2022-03-11

Pourquoi les moteurs Rails ne sont-ils pas utilisés plus souvent ? Je ne connais pas la réponse, mais je pense que la généralisation de « Tout est un moteur » a masqué les domaines problématiques qu'ils peuvent aider à résoudre.

La superbe documentation Rails Guide pour démarrer avec Rails Engines fait référence à quatre exemples populaires d'implémentations de Rails Engine : Forem, Devise, Spree et RefineryCMS. Ce sont des cas d'utilisation réels fantastiques pour les moteurs, chacun utilisant une approche différente de l'intégration avec une application Rails.

Chaque guide Rails devrait couvrir le sujet des modèles de conception des moteurs Rails et leurs exemples.

L'examen de certaines parties de la configuration et de la composition de ces gemmes donnera aux développeurs avancés de Ruby on Rails une connaissance précieuse des modèles ou des techniques qui sont essayés et testés dans la nature. Ainsi, le moment venu, vous pourrez avoir quelques options supplémentaires à évaluer.

Je m'attends à ce que vous ayez une connaissance superficielle du fonctionnement d'un moteur, donc si vous sentez que quelque chose ne va pas, veuillez parcourir l'excellent guide Rails Getting Started With Engines .

Sans plus tarder, aventurons-nous dans le monde sauvage des exemples de moteurs Rails !

Forem

Un moteur pour Rails qui vise à être le meilleur petit système de forum de tous les temps

Ce bijou suit à la lettre la direction du Rails Guide on Engines. C'est un exemple considérable et la lecture de son référentiel vous donnera une idée de jusqu'où vous pouvez étendre la configuration de base.

Il s'agit d'un bijou à moteur unique qui utilise quelques techniques pour s'intégrer à l'application principale.

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

La partie intéressante ici est le Decorators.register! méthode de classe, exposée par la gemme Decorators. Il encapsule le chargement de fichiers qui ne seraient pas inclus dans le processus de chargement automatique de Rails. Vous vous souvenez peut-être que l'utilisation d'instructions require explicites ruine le rechargement automatique en mode développement, c'est donc une bouée de sauvetage ! Il sera plus clair d'utiliser l'exemple du Guide pour illustrer ce qui se passe :

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

La plupart de la magie de la configuration du Forem se produit dans la définition du module principal du Forem . Ce fichier repose sur une variable user_class définie dans un fichier d'initialisation :

 Forem.user_class = "User"

Vous accomplissez cela en utilisant mattr_accessor mais tout est dans le Rails Guide donc je ne le répéterai pas ici. Ceci mis en place, le Forem agrémente alors la classe utilisateur avec tout ce dont elle a besoin pour faire fonctionner son application :

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

Ce qui s'avère être beaucoup ! J'ai coupé la majorité mais j'ai laissé une définition d'association ainsi qu'une méthode d'instance pour vous montrer le type de lignes que vous pouvez y trouver.

Un aperçu de l'ensemble du fichier peut vous montrer à quel point le portage d'une partie de votre application pour la réutilisation sur un moteur peut être gérable.

La décoration est le nom du jeu dans l'utilisation par défaut du moteur. En tant qu'utilisateur final de la gemme, vous pouvez remplacer le modèle, la vue et les contrôleurs en créant vos propres versions des classes à l'aide des chemins de fichier et des conventions de dénomination des fichiers définies dans le LISEZMOI de la gemme décoratrice. Il y a cependant un coût associé à cette approche, en particulier lorsque le moteur fait l'objet d'une mise à niveau de version majeure - la maintenance du fonctionnement de vos décorations peut rapidement devenir incontrôlable. Je ne cite pas le Forem ici, je pense qu'ils sont déterminés à conserver une fonctionnalité de base soudée, mais gardez cela à l'esprit si vous créez un moteur et décidez d'opter pour une refonte.

Récapitulons celui-ci : il s'agit du modèle de conception du moteur Rails par défaut qui repose sur les utilisateurs finaux qui décorent les vues, les contrôleurs et les modèles, ainsi que sur la configuration des paramètres de base via des fichiers d'initialisation. Cela fonctionne bien pour les fonctionnalités très ciblées et connexes.

Concevoir

Une solution d'authentification flexible pour Rails

Vous constaterez qu'un moteur est très similaire à une application Rails, avec des répertoires de views , de controllers et de models . Devise est un bon exemple d'encapsulation d'une application et d'exposition d'un point d'intégration pratique. Voyons comment cela fonctionne exactement.

Vous reconnaîtrez ces lignes de code si vous êtes développeur Rails depuis plus de quelques semaines :

 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

Chaque paramètre passé à la méthode de devise représente un module dans le moteur de conception. Au total, dix de ces modules héritent du familier ActiveSupport::Concern . Celles-ci étendent votre classe User en invoquant la méthode devise dans sa portée.

Avoir ce type de point d'intégration est très flexible, vous pouvez ajouter ou supprimer n'importe lequel de ces paramètres pour modifier le niveau de fonctionnalité que vous souhaitez que le moteur exécute. Cela signifie également que vous n'avez pas besoin de coder en dur le modèle que vous souhaitez utiliser dans un fichier d'initialisation, comme suggéré par le Rails Guide on Engines. En d'autres termes, ce n'est pas nécessaire :

 Devise.user_model = 'User'

Cette abstraction signifie également que vous pouvez l'appliquer à plusieurs modèles d'utilisateurs au sein de la même application ( admin et user par exemple), alors que l'approche du fichier de configuration vous laisserait lié à un seul modèle avec authentification. Ce n'est pas le principal argument de vente, mais cela illustre une manière différente de résoudre un problème.

Devise étend ActiveRecord::Base avec son propre module qui inclut la devise de la méthode de conception :

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

Toute classe héritant d' ActiveRecord::Base aura désormais accès aux méthodes de classe définies dans 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

( J'ai supprimé beaucoup de code ( # ... ) pour mettre en évidence les parties importantes. )

En paraphrasant le code, pour chaque nom de module passé à la méthode devise nous sommes :

  • charger le module que nous avons spécifié qui vit sous Devise::Models ( Devise::Models.const_get(m.to_s.classify )
  • étendre la classe User avec le module ClassMethods s'il en a un
  • inclure le module spécifié ( include mod ) pour ajouter ses méthodes d'instance à la classe appelant la méthode de devise ( User )

Si vous vouliez créer un module qui pourrait être chargé de cette manière, vous auriez besoin de vous assurer qu'il suivait l'interface ActiveSupport::Concern habituelle, mais de l'espacer de noms sous Devise:Models car c'est là que nous cherchons à récupérer la constante :

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

Phew.

Si vous avez déjà utilisé Rails' Concerns et expérimenté la possibilité de réutilisation qu'ils offrent, alors vous pouvez apprécier les subtilités de cette approche. En bref, décomposer les fonctionnalités de cette manière facilite les tests en étant abstrait d'un modèle ActiveRecord , et a un surcoût inférieur au modèle par défaut utilisé par le Forem lorsqu'il s'agit d'étendre les fonctionnalités.

Ce modèle consiste à décomposer votre fonctionnalité en Rails Concerns et à exposer un point de configuration pour les inclure ou les exclure dans une portée donnée. Un moteur formé de cette manière est pratique pour l'utilisateur final - un facteur contribuant au succès et à la popularité de Devise. Et maintenant, vous savez comment le faire aussi !

Fête

Une solution e-commerce open source complète pour Ruby on Rails

Spree a déployé un effort colossal pour maîtriser son application monolithique en passant à l'utilisation de moteurs. La conception de l'architecture avec laquelle ils roulent maintenant est une gemme "Spree" qui contient de nombreuses gemmes Engine.

Ces moteurs créent des partitions au comportement que vous avez peut-être l'habitude de voir dans une application monolithique ou réparties sur plusieurs applications :

  • spree_api (API RESTful)
  • spree_frontend (composants destinés à l'utilisateur)
  • spree_backend (zone d'administration)
  • spree_cmd (Outils de ligne de commande)
  • spree_core (Modèles et expéditeurs, les composants de base de Spree sans lesquels il ne peut pas fonctionner)
  • spree_sample (exemple de données)

La gemme englobante les assemble, laissant au développeur le choix du niveau de fonctionnalité requis. Par exemple, vous pouvez exécuter uniquement le moteur spree_core et envelopper votre propre interface autour de lui.

Le joyau principal de la Spree nécessite ces moteurs :

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

Chaque moteur doit ensuite personnaliser son engine_name et son chemin root (ce dernier pointant généralement vers la gemme de niveau supérieur) et se configurer en s'accrochant au processus d'initialisation :

 # 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

Vous pouvez ou non reconnaître cette méthode d'initialisation : elle fait partie de Railtie et est un crochet qui vous donne la possibilité d'ajouter ou de supprimer des étapes de l'initialisation du framework Rails. Spree s'appuie fortement sur ce crochet pour configurer son environnement complexe pour tous ses moteurs.

En utilisant l'exemple ci-dessus lors de l'exécution, vous aurez accès à vos paramètres en accédant à la constante Rails de niveau supérieur :

 Rails.application.config.spree

Avec ce guide de modèle de conception de moteur Rails ci-dessus, nous pourrions l'appeler un jour, mais Spree a une tonne de code incroyable, alors plongeons dans la façon dont ils utilisent l'initialisation pour partager la configuration entre les moteurs et l'application Rails principale.

Spree a un système de préférences complexe qu'il charge en ajoutant une étape dans le processus d'initialisation :

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

Ici, nous attachons à app.config.spree une nouvelle instance Spree::Core::Environment . Dans l'application rails, vous pourrez y accéder via Rails.application.config.spree de n'importe où - modèles, contrôleurs, vues.

En descendant, la classe Spree::Core::Environment que nous créons ressemble à ceci :

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

Il expose une variable :preferences définie sur une nouvelle instance de la classe Spree::AppConfiguration , qui à son tour utilise une méthode de preference définie dans la classe Preferences::Configuration pour définir des options avec des valeurs par défaut pour la configuration générale de l'application :

 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

Je ne montrerai pas le fichier Preferences::Configuration car cela prendra un peu d'explications, mais il s'agit essentiellement d'un sucre syntaxique pour obtenir et définir des préférences. (En vérité, il s'agit d'une simplification excessive de sa fonctionnalité, car le système de préférences enregistrera des valeurs autres que les valeurs par défaut pour les préférences existantes ou nouvelles dans la base de données, pour toute classe ActiveRecord avec une colonne :preference - mais vous n'avez pas besoin de sachez que.)

Voici l'une de ces options en action :

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

Les calculatrices contrôlent toutes sortes de choses dans Spree - frais d'expédition, promotions, ajustements des prix des produits - donc avoir un mécanisme pour les échanger de cette manière augmente l'extensibilité du moteur.

L'une des nombreuses façons de remplacer les paramètres par défaut de ces préférences consiste à utiliser un initialiseur dans l'application Rails principale :

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

Si vous avez lu le RailsGuide sur les moteurs, considéré leurs modèles de conception ou construit vous-même un moteur, vous saurez qu'il est trivial d'exposer un setter dans un fichier d'initialisation pour que quelqu'un l'utilise. Alors vous vous demandez peut-être pourquoi tout ce remue-ménage avec le système de configuration et de préférences ? N'oubliez pas que le système de préférences résout un problème de domaine pour Spree. L'accrochage au processus d'initialisation et l'accès au framework Rails pourraient vous aider à répondre à vos exigences de manière maintenable.

Ce modèle de conception de moteur se concentre sur l'utilisation du framework Rails comme constante entre ses nombreuses pièces mobiles pour stocker les paramètres qui ne changent pas (généralement) au moment de l'exécution, mais changent entre les installations d'applications.

Si vous avez déjà essayé de mettre en marque blanche une application Rails, vous connaissez peut-être ce scénario de préférences et avez ressenti la douleur des tables de « paramètres » de base de données alambiquées dans un long processus de configuration pour chaque nouvelle application. Maintenant, vous savez qu'un chemin différent est disponible et c'est génial !

RaffinerieCMS

Un système de gestion de contenu open source pour Rails

Convention sur configuration quelqu'un? Rails Engines peut certainement ressembler plus à un exercice de configuration à certains moments, mais RefineryCMS se souvient d'une partie de cette magie Rails. Voici l'intégralité du contenu de son répertoire 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

Wow. Si vous ne pouvez pas le dire, l'équipe de Refinery sait vraiment ce qu'elle fait. Ils roulent avec le concept d'une extension qui est essentiellement un autre moteur. Comme Spree, il possède un joyau de couture englobant, mais n'utilise que deux points et rassemble une collection de moteurs pour offrir son ensemble complet de fonctionnalités.

Des extensions sont également créées par les utilisateurs du moteur, pour créer leur propre mélange de fonctionnalités CMS pour les blogs, les actualités, les portefeuilles, les témoignages, les demandes de renseignements, etc. (c'est une longue liste), toutes connectées au cœur de RefineryCMS.

Cette conception peut attirer votre attention pour son approche modulaire, et Refinery est un excellent exemple de ce modèle de conception Rails. "Comment ça marche?" Je vous entends demander.

Le moteur core trace quelques crochets que les autres moteurs peuvent utiliser :

 # 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

Comme vous pouvez le voir, before_inclusion et after_inclusion stockent simplement une liste de procs qui seront exécutés plus tard. Le processus d'inclusion de Refinery étend les applications Rails actuellement chargées avec les contrôleurs et les assistants de Refinery. En voici un en action :

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

Je suis sûr que vous avez déjà mis des méthodes d'authentification dans votre ApplicationController et AdminController , c'est une façon programmatique de le faire.

Regarder le reste de ce fichier Authentication Engine nous aidera à glaner quelques autres composants clés :

 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

Sous le capot, les extensions Refinery utilisent un système de Plugin . L'étape d' initializer vous semblera familière à partir de l'analyse du code Spree. Ici, nous répondons simplement aux exigences des méthodes de register à ajouter à la liste de Refinery::Plugins dont l'extension core assure le suivi, et Refinery.register_extension ajoute simplement le nom du module. à une liste stockée dans un accesseur de classe.

Voici un choc : la classe Refinery::Authentication est vraiment un wrapper autour de Devise, avec quelques personnalisations. Donc c'est des tortues tout en bas !

Les extensions et les plugins sont des concepts que Refinery a développés pour prendre en charge leur riche écosystème d'applications et d'outils mini-rails - pensez rake generate refinery:engine . Le modèle de conception ici diffère de Spree en imposant une API supplémentaire autour du moteur Rails pour aider à gérer leur composition.

L'idiome "The Rails Way" est au cœur de Refinery, de plus en plus présent dans leurs applications mini-rails, mais de l'extérieur, vous ne le sauriez pas. Concevoir des limites au niveau de la composition de l'application est aussi important, voire plus, que de créer une API propre pour vos classes et modules utilisés dans vos applications Rails.

L'encapsulation de code sur lequel vous n'avez pas de contrôle direct est un modèle courant, c'est une prévoyance pour réduire le temps de maintenance lorsque ce code change, limitant le nombre d'endroits où vous devrez apporter des modifications pour prendre en charge les mises à niveau. L'application de cette technique parallèlement à la fonctionnalité de partitionnement crée une plate-forme flexible pour la composition, et voici un exemple concret sous votre nez - je dois aimer l'open source !

Conclusion


Nous avons vu quatre approches pour concevoir des modèles de moteur Rails en analysant des gemmes populaires utilisées dans des applications du monde réel. Il vaut la peine de lire leurs référentiels pour apprendre d'une riche expérience déjà appliquée et itérée. Debout sur les épaules des géants.

Dans ce guide Rails, nous nous sommes concentrés sur les modèles de conception et les techniques d'intégration des moteurs Rails et des applications Rails de leurs utilisateurs finaux, afin que vous puissiez ajouter la connaissance de ceux-ci à votre ceinture d'outils Rails.

J'espère que vous avez appris autant que moi en examinant ce code et que vous vous sentez inspiré pour donner une chance à Rails Engines lorsqu'ils correspondent à la facture. Un grand merci aux mainteneurs et aux contributeurs des gemmes que nous avons examinées. Excellent travail les gens !

En relation: Timestamp Troncature: A Ruby on Rails ActiveRecord Tale