Le modèle Publish-Subscribe on Rails : un didacticiel de mise en œuvre
Publié: 2022-03-11Le modèle de publication-abonnement (ou pub/sub, en abrégé) est un modèle de messagerie Ruby on Rails dans lequel les expéditeurs de messages (éditeurs) ne programment pas les messages à envoyer directement à des destinataires spécifiques (abonnés). Au lieu de cela, le programmeur "publie" des messages (événements), sans aucune connaissance des abonnés qu'il peut y avoir.
De même, les abonnés expriment leur intérêt pour un ou plusieurs événements, et ne reçoivent que les messages qui les intéressent, à l'insu des éditeurs.
Pour ce faire, un intermédiaire, appelé « courtier de messages » ou « bus d'événements », reçoit les messages publiés, puis les transmet aux abonnés qui sont enregistrés pour les recevoir.
En d'autres termes, pub-sub est un modèle utilisé pour communiquer des messages entre différents composants du système sans que ces composants ne sachent quoi que ce soit sur l'identité de l'autre.
Ce modèle de conception n'est pas nouveau, mais il n'est pas couramment utilisé par les développeurs Rails. De nombreux outils permettent d'intégrer ce modèle de conception dans votre base de code, tels que :
- Wisper (que je préfère personnellement et dont je discuterai plus loin)
- EventBus
- EventBGBus (un fork d'EventBus)
- LapinMQ
- Redis
Tous ces outils ont différentes implémentations pub-sub sous-jacentes, mais ils offrent tous les mêmes avantages majeurs pour une application Rails.
Avantages de la mise en œuvre Pub-Sub
Réduction du ballonnement du modèle/contrôleur
C'est une pratique courante, mais pas une bonne pratique, d'avoir des modèles ou des contrôleurs gras dans votre application Rails.
Le modèle pub/sub peut facilement aider à décomposer les gros modèles ou contrôleurs.
Moins de rappels
Avoir beaucoup de rappels entrelacés entre les modèles est une odeur de code bien connue, et petit à petit, cela couple étroitement les modèles, ce qui les rend plus difficiles à maintenir ou à étendre.
Par exemple, un modèle Post pourrait ressembler à ceci :
# app/models/post.rb class Post # ... field: content, type: String # ... after_create :create_feed, :notify_followers # ... def create_feed Feed.create!(self) end def notify_followers User::NotifyFollowers.call(self) end end Et le contrôleur Post pourrait ressembler à ceci :
# app/controllers/api/v1/posts_controller.rb class Api::V1::PostsController < Api::V1::ApiController # ... def create @post = current_user.posts.build(post_params) if @post.save render_created(@post) else render_unprocessable_entity(@post.errors) end end # ... end Comme vous pouvez le constater, le modèle de Post comporte des rappels qui associent étroitement le modèle au modèle de Feed et au service ou à la préoccupation User::NotifyFollowers . En utilisant n'importe quel modèle pub/sub, le code précédent pourrait être refactorisé pour ressembler à ce qui suit, qui utilise Wisper :
# app/models/post.rb class Post # ... field: content, type: String # ... # no callbacks in the models! endLes éditeurs publient l'événement avec l'objet de charge utile d'événement qui peut être nécessaire.
# app/controllers/api/v1/posts_controller.rb # corresponds to the publisher in the previous figure class Api::V1::PostsController < Api::V1::ApiController include Wisper::Publisher # ... def create @post = current_user.posts.build(post_params) if @post.save # Publish event about post creation for any interested listeners publish(:post_create, @post) render_created(@post) else # Publish event about post error for any interested listeners publish(:post_errors, @post) render_unprocessable_entity(@post.errors) end end # ... endLes abonnés ne souscrivent qu'aux événements auxquels ils souhaitent répondre.
# app/listener/feed_listener.rb class FeedListener def post_create(post) Feed.create!(post) end end # app/listener/user_listener.rb class UserListener def post_create(post) User::NotifyFollowers.call(self) end endEvent Bus enregistre les différents abonnés du système.
# config/initializers/wisper.rb Wisper.subscribe(FeedListener.new) Wisper.subscribe(UserListener.new) Dans cet exemple, le modèle pub-sub a complètement éliminé les rappels dans le modèle Post et a aidé les modèles à fonctionner indépendamment les uns des autres avec une connaissance minimale les uns des autres, garantissant un couplage lâche. Étendre le comportement à des actions supplémentaires consiste simplement à se connecter à l'événement souhaité.
Le principe de responsabilité unique (SRP)
Le principe de responsabilité unique est vraiment utile pour maintenir une base de code propre. Le problème de s'y tenir est que parfois la responsabilité de la classe n'est pas aussi claire qu'elle devrait l'être. Ceci est particulièrement courant lorsqu'il s'agit de MVC (comme Rails).
Les modèles doivent gérer la persistance, les associations et pas grand-chose d'autre.
Les contrôleurs doivent gérer les demandes des utilisateurs et être un wrapper autour de la logique métier (objets de service).
Les objets de service doivent encapsuler l'une des responsabilités de la logique métier, fournir un point d'entrée pour les services externes ou agir comme une alternative aux problèmes de modèle.
Grâce à sa capacité à réduire le couplage, le modèle de conception pub-sub peut être combiné avec des objets de service à responsabilité unique (SRSO) pour aider à encapsuler la logique métier et empêcher la logique métier de se glisser dans les modèles ou les contrôleurs. Cela permet de garder la base de code propre, lisible, maintenable et évolutive.
Voici un exemple de logique métier complexe mise en œuvre à l'aide du modèle pub/sub et des objets de service :
Éditeur
# app/service/financial/order_review.rb class Financial::OrderReview include Wisper::Publisher # ... def self.call(order) if order.approved? publish(:order_create, order) else publish(:order_decline, order) end end # ...Les abonnés
# app/listener/client_listener.rb class ClientListener def order_create(order) # can implement transaction using different service objects Client::Charge.call(order) Inventory::UpdateStock.call(order) end def order_decline(order) Client::NotifyDeclinedOrder(order) end endEn utilisant le modèle de publication-abonnement, la base de code s'organise presque automatiquement en SRSO. De plus, la mise en œuvre du code pour les flux de travail complexes est facilement organisée autour d'événements, sans sacrifier la lisibilité, la maintenabilité ou l'évolutivité.
Essai
En décomposant les gros modèles et les contrôleurs, et en ayant beaucoup de SRSO, tester la base de code devient un processus beaucoup, beaucoup plus facile. C'est particulièrement le cas lorsqu'il s'agit de tests d'intégration et de communication inter-modules. Les tests doivent simplement s'assurer que les événements sont publiés et reçus correctement.
Wisper a un joyau de test qui ajoute des matchers RSpec pour faciliter le test de différents composants.

Dans les deux exemples précédents (exemple de Post et exemple de Order ), les tests doivent inclure les éléments suivants :
Éditeurs
# spec/service/financial/order_review.rb describe Financial::OrderReview do it 'publishes :order_create' do @order = Fabricate(:order, approved: true) expect { Financial::OrderReview.call(@order) }.to broadcast(:order_create) end it 'publishes :order_decline' do @order = Fabricate(:order, approved: false) expect { Financial::OrderReview.call(@order) }.to broadcast(:order_decline) end endLes abonnés
# spec/listeners/feed_listener_spec.rb describe FeedListener do it 'receives :post_create event on PostController#create' do expect(FeedListner).to receive(:post_create).with(Post.last) post '/post', { content: 'Some post content' }, request_headers end endCependant, il existe certaines limites au test des événements publiés lorsque l'éditeur est le contrôleur.
Si vous voulez aller plus loin, faire tester également la charge utile aidera à maintenir une base de code encore meilleure.
Comme vous pouvez le constater, les tests de modèles de conception pub-sub sont simples. Il s'agit simplement de s'assurer que les différents événements sont correctement publiés et reçus.
Performance
C'est plus un avantage possible . Le modèle de conception de publication-abonnement lui-même n'a pas d'impact inhérent majeur sur les performances du code. Cependant, comme pour tout outil que vous utilisez dans votre code, les outils de mise en œuvre de pub/sub peuvent avoir un effet important sur les performances. Parfois, cela peut être un mauvais effet, mais parfois cela peut être très bon.
Tout d'abord, un exemple d'effet néfaste : Redis est un "cache et magasin de valeurs clés avancés". Il est souvent appelé serveur de structure de données. Cet outil populaire prend en charge le modèle pub/sub et est très stable. Cependant, s'il est utilisé sur un serveur distant (pas le même serveur sur lequel l'application Rails est déployée), cela entraînera une énorme perte de performances en raison de la surcharge du réseau.
D'autre part, Wisper dispose de divers adaptateurs pour la gestion des événements asynchrones, comme wisper-celluloid, wisper-sidekiq et wisper-activejob. Ces outils prennent en charge les événements asynchrones et les exécutions par thread. qui, s'il est appliqué correctement, peut considérablement augmenter les performances de l'application.
L'essentiel
Si vous visez des performances supplémentaires, le modèle pub/sub pourrait vous aider à l'atteindre. Mais même si vous ne trouvez pas d'amélioration des performances avec ce modèle de conception Rails, cela aidera toujours à garder le code organisé et à le rendre plus maintenable. Après tout, qui peut s'inquiéter des performances d'un code qui ne peut pas être maintenu ou qui ne fonctionne pas ?
Inconvénients de la mise en œuvre Pub-Sub
Comme pour toutes choses, le modèle pub-sub présente également certains inconvénients.
Couplage lâche (couplage sémantique inflexible)
Les plus grandes forces du modèle pub/sub sont aussi ses plus grandes faiblesses. La structure des données publiées (la charge utile de l'événement) doit être bien définie, et devient rapidement assez rigide. Afin de modifier la structure des données de la charge utile publiée, il est nécessaire de connaître tous les abonnés, et soit de les modifier également, soit de s'assurer que les modifications sont compatibles avec les anciennes versions. Cela rend la refactorisation du code Publisher beaucoup plus difficile.
Si vous voulez éviter cela, vous devez être très prudent lors de la définition de la charge utile des éditeurs. Bien sûr, si vous avez une excellente suite de tests, qui teste la charge utile ainsi que mentionné précédemment, vous n'avez pas à vous soucier de la panne du système après avoir modifié la charge utile ou le nom de l'événement de l'éditeur.
Stabilité du bus de messagerie
Les éditeurs n'ont aucune connaissance du statut de l'abonné et vice versa. En utilisant de simples outils pub/sub, il peut ne pas être possible d'assurer la stabilité du bus de messagerie lui-même et de s'assurer que tous les messages publiés sont correctement mis en file d'attente et livrés.
Le nombre croissant de messages échangés entraîne des instabilités dans le système lors de l'utilisation d'outils simples, et il peut ne pas être possible d'assurer la livraison à tous les abonnés sans certains protocoles plus sophistiqués. En fonction du nombre de messages échangés et des paramètres de performances que vous souhaitez atteindre, vous pouvez envisager d'utiliser des services tels que RabbitMQ, PubNub, Pusher, CloudAMQP, IronMQ ou de nombreuses autres alternatives. Ces alternatives offrent des fonctionnalités supplémentaires et sont plus stables que Wisper pour les systèmes plus complexes. Cependant, leur mise en œuvre nécessite également un travail supplémentaire. Vous pouvez en savoir plus sur le fonctionnement des courtiers de messages ici
Boucles d'événements infinies
Lorsque le système est entièrement piloté par des événements, vous devez faire très attention à ne pas avoir de boucles d'événements. Ces boucles sont comme les boucles infinies qui peuvent se produire dans le code. Cependant, ils sont plus difficiles à détecter à l'avance et peuvent paralyser votre système. Ils peuvent exister sans votre préavis lorsque de nombreux événements sont publiés et souscrits dans le système.
Conclusion du didacticiel sur les rails
Le modèle de publication-abonnement n'est pas une solution miracle pour tous vos problèmes Rails et odeurs de code, mais c'est un très bon modèle de conception qui aide à découpler différents composants du système et à le rendre plus maintenable, lisible et évolutif.
Lorsqu'il est combiné avec des objets de service à responsabilité unique (SRSO), le pub-sub peut également aider à encapsuler la logique métier et à empêcher différentes préoccupations métier de se glisser dans les modèles ou les contrôleurs.
Le gain de performances après l'utilisation de ce modèle dépend principalement de l'outil sous-jacent utilisé, mais le gain de performances peut être amélioré de manière significative dans certains cas, et dans la plupart des cas, cela ne nuira certainement pas aux performances.
Cependant, l'utilisation du modèle pub-sub doit être étudiée et planifiée avec soin, car avec la grande puissance du couplage lâche vient la grande responsabilité de maintenir et de refactoriser les composants faiblement couplés.
Étant donné que les événements peuvent facilement devenir incontrôlables, une simple bibliothèque pub/sub peut ne pas garantir la stabilité du courtier de messages.
Et enfin, il y a le danger d'introduire des boucles d'événements infinies qui passent inaperçues jusqu'à ce qu'il soit trop tard.
J'utilise ce modèle depuis presque un an maintenant, et il m'est difficile d'imaginer écrire du code sans lui. Pour moi, c'est la colle qui fait que les travaux d'arrière-plan, les objets de service, les préoccupations, les contrôleurs et les modèles communiquent tous proprement entre eux et fonctionnent ensemble comme un charme.
J'espère que vous avez appris autant que moi en examinant ce code et que vous vous sentez inspiré pour donner au modèle Publish-Subscribe une chance de rendre votre application Rails géniale.
Enfin, un grand merci à @krisleech pour son formidable travail de mise en œuvre de Wisper.
