Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
April 10, 2023 06:22 pm GMT

How to implement Pub/Sub pattern in Ruby on Rails?

Overview:

In this article we'll provide a comprehensive guide to understanding and implementing the Pub/Sub pattern. We will explore the evolution of this pattern from a primitive implementation to three product-ready solutions: ActiveSupport::Notifications, Wisper, and Dry-Events. We will begin by discussing the core concept of the Pub/Sub pattern. Then:

  • We will provide a step-by-step guide on how to implement the Pub/Sub pattern using the basic Ruby language constructs.
  • We will introduce the ActiveSupport::Notifications library.
  • We will delve into the Wisper gem.
  • Finally, we will examine the Dry-Events gem.

By the end of this article, we will have a solid understanding of the Pub/Sub pattern and the different options available for implementing it in their Ruby on Rails applications.

Definition

In simple terms, the Pub/Sub (Publish/Subscribe) pattern is a way of decoupling components in a system by providing an alternative method of communication between them. Rather than sending messages directly to specific components, a publisher broadcasts messages to any subscribers that are interested in receiving them. In other words, the Pub/Sub pattern enables communication between different components of a system without these components having any knowledge of each other.

Why do we need it and what problems can this pattern solve?

Let's take a look at the following piece of code from the rails controller.

def create  animal = Animal.create(animal_params)  # send_email  # send_mobile_notification  # save_logs  render json: animalend

What are we doing here?

  • We want to create an Animal record and show this record in the response
  • PLUS send an email about it
  • PLUS send a mobile notification about it
  • PLUS save this action to the logs

Do we have any problems with this approach?

Yes, we have. This code can lead to several problems, including:

  • Reduced maintainability: With multiple unrelated tasks in the same controller action, it can be difficult to maintain or modify the code in the future.
  • Reduced testability: Testing the create action becomes more complicated due to the additional responsibilities it has. It also makes it more challenging to test these other responsibilities in isolation.
  • Reduced scalability: If additional functionality needs to be added, it would likely be added to the create action, which can make the controller even more complicated and challenging to maintain.
  • Reduced readability: Mixing unrelated functionality in the same controller action can make it difficult to understand the overall purpose of the action.

How can we solve this problem?

Some developers solve this problem by using callbacks on the model to handle it. So, they might use something like this::

# app/models/animal.rb# frozen_string_literal: trueclass Animal < ApplicationRecord  after_create :send_email, :send_mobile_notification, :save_logs  private  def send_email    puts 'Email will be sent'  end  def send_mobile_notification    puts 'Mobile Notification will be sent'  end  def save_logs    puts 'Logs will be saved'  endend

But callbacks can potentially lead to significant issues, so it is better to avoid them.

Potentially, we could create a separate service object and place all this logic inside it. For example:

# app/units/create_animal_service.rbclass CreateAnimalService  def self.call    # do something    send_email    send_mobile_notification    save_logs  end  private  def send_email    puts 'Email will be sent'  end  def send_mobile_notification    puts 'Mobile Notification will be sent'  end  def save_logs    puts 'Logs will be saved'  endend

While this approach is an improvement over using model callbacks, there is still a significant amount of cohesion between the core business logic and non-critical secondary logic. As a result, developers must understand the secondary logic, which is closely tied to the core logic, leading to increased cognitive load and a shift in focus from the primary part to technical details.

To address these issues, we can use the Pub/Sub pattern to separate the core business logic from non-critical secondary logic. Let's discuss this further:

How pub/sub actually works?

There are three key elements that must be implemented to make Pub/Sub work for you:

  • Send/Publish/Broadcast an event.
  • Define the logic that can potentially react to this event.
  • Bind the event with the logic ("subscribe to the event").

And we would like to have something like this as our new interface:

def create  animal = Animal.create(animal_params).tap { send_event('animal_created') }  render json: animalend

This interface would allow us to concentrate on the core business logic and delegate all 'secondary' logic to somewhere else.

With the key elements above in mind, let's try to build a primitive Pub/Sub logic using plain Ruby to understand the concept behind it.

Primitive Ruby implementation

In this implementation, we'll use the same three main elements: 'Publishing an event', 'Defining logic' and 'Binding the event to the logic'.

Publishing an Event

For example, let's consider an event named animal_created, which is represented as a string. Whenever this event is sent, we simply add this string to an array

events = []events << 'animal_created'

Defining logic to react to the event (subcriptions)

To react to the event, we need to define some actions that can potentially happen when the event occurs. These actions are called subscriptions or listeners. We can use a simple hash with two keys: event_name and action. event_name is a string that identifies the event, and action is a proc that is executed when the event is received.

subscriptions = []subscriptions << { event_name: 'animal_created', action: -> { puts 'hello animal!' } }subscriptions << { event_name: 'animal_created', action: -> { puts 'hello animal 2!' } }

Binding events and subscriptions

We have defined two main elements, but there is a problem - events and subscriptions are not connected and do not know about each other. To address this, we need to introduce some logic that will iterate over new events and match them with the appropriate action defined in the subscriptions.

events.each do |event_name|  subscriptions.select { |subscription| subscription[:event_name] == event_name }.each { |subscription| subscription[:action].call }endevents.clear

So, that is the basic idea. Let's try to improve our implementation by encapsulating some logic in a class.

Primitive Ruby implementation using class

We will create a class called Notification, which will have 3 methods: publish, subscribe and initialize. In this implementation, we will immediately react to the published event as soon as it is sent.

class Notification  def initialize    # We are initializing a Hash with empty arrays as default values for any new key    @notifications = Hash.new { |hash, key| hash[key] = [] }  end  def subscribe(event_name, action:)    notifications[event_name] << action  end  def publish(event_name)    notifications[event_name].each { |action| action.call }  end  private  attr_reader :notificationsend

Here is a usage example:

# Initialize the classnotification = Notification.new# Subscribe to the event "animal_created" with two actions.notification.subscribe('animal_created', action: -> { puts 'hello animal!' })notification.subscribe('animal_created', action: -> { puts 'hello animal 2!' })# Publish the 'animal_created' event, which will trigger both actions.notification.publish('animal_created')

Out three key elements have also been used in this implementation.

This is a very simple example of how pub/sub can work. However, there are many ready-made solutions that we can use in our application. Let's consider three of them:

  • ActiveSupport::Notification
  • Wisper
  • Dry-events

They're pretty much the same and have very similar interfaces. Let's look at each of these options one by one to better understand the concept.

ActiveSupport::Notification

ActiveSupport::Notification has the same three key elements that we discussed earlier

  • Send/Publish/Broadcast an event.
  • Define the logic that can potentially react to this event.
  • Bind the event with the logic ("subscribe to the event").

Let's consider three ways of implementing the pub/sub pattern using ActiveSupport::Notification, starting from the simplest and progressing to the most complex, in order to better understand the concept:

  • ActiveSupport::Notification with blocks
  • ActiveSupport::Notification with classes
  • ActiveSupport::Notification in a Rails application

Let's start with the most basic one

ActiveSupport::Notification with blocks

# Define the logicActiveSupport::Notifications.subscribe('animal_created') do |*args|  puts "New animal created 1"endActiveSupport::Notifications.subscribe('animal_created') do |*args|  puts "New animal created 2"end# Send the eventActiveSupport::Notifications.instrument('animal_created')# => New animal created 1# => New animal created 2

So, here everything looks obvious. Basically, we have the same interface as we described in the primitive Ruby implementation. Let's go further.

ActiveSupport::Notification with classes

Instead of a block, we can also send a class that has a call method. This can be useful when your logic becomes too complex, and using classes can greatly simplify testing.

# Define the logicclass Subscription1  def call(name, started, finished, unique_id, payload)    puts "New animal created 1"  endendclass Subscription2  def call(name, started, finished, unique_id, payload)    puts "New animal created 2"  endend# Bind the event with the logicActiveSupport::Notifications.subscribe('animal_created', Subscription1.new)ActiveSupport::Notifications.subscribe('animal_created', Subscription2.new)# Send the eventActiveSupport::Notifications.instrument('animal_created')# => New animal created 1# => New animal created 2

Here's how ActiveSupport::Notification basically works. Let's try to add it to a real Rails application.

ActiveSupport::Notification in a Rails application

For example, we have a service object that performs some actions and sends the 'animal_created' event.

Publishing an Event

# app/units/service.rbclass Service  def self.call    # perform some actions    ActiveSupport::Notifications.instrument('animal_created', payload: {})  endend

Defining logic to react to the event

Additionally, we need to create subscriptions to react to these events. We want to send an email, send a mobile notification, and log this action somewhere. To accomplish this, we'll create three subscription classes, each of which will handle one of these tasks.

  • EmailSubscription
# app/subscriptions/email_subscription.rb# frozen_string_literal: trueclass EmailSubscription < Subscription  def animal_created(payload)    puts "Email will be sent #{payload}"  endend
  • MobileNotificationSubscription
# app/subscriptions/mobile_notification_subscription.rb# frozen_string_literal: trueclass MobileNotificationSubscription < Subscription  def animal_created(payload)    puts "Mobile Notification will be sent #{payload}"  endend
  • LoggerSubscription
# app/subscriptions/logger_subscription.rb# frozen_string_literal: trueclass LoggerSubscription < Subscription  def animal_created(payload)    puts "Logs will be saved #{payload}"  endend

Every subscription class should include the desired reaction to the event and a method call to actually run this reaction. That's why we've created a parent class Subscription.

  • Subscription
# lib/subscription.rb# frozen_string_literal: trueclass Subscription  def call(event_name, _, _, _, payload)    send(event_name, payload)  endend

Binding events and logic

We've created an event that can be sent and classes with the appropriate reaction to the event. However, these classes aren't currently connected to the event, meaning they won't react to it. To establish the connection, we'll use the following logic after Rails initialization.

# config/initializers/subscriptions.rb# frozen_string_literal: trueRails.application.config.after_initialize do  subscriptions = {    EmailSubscription: ['animal_created'],    LoggerSubscription: ['animal_created'],    MobileNotificationSubscription: ['animal_created']  }  # We iterate over each subscription class and bind its logic with the event name, similar to what we did in the example with blocks.  subscriptions.each do |subscription_class, events|    events.each do |event|      ActiveSupport::Notifications.subscribe(event, subscription_class.to_s.constantize.new)    end  endend

So now we can call Service.call and this code will send the 'animal_created' event, triggering all subscriptions:

Email will be sent {:payload=>{}}Logs will be saved {:payload=>{}}Mobile Notification will be sent {:payload=>{}}

Adding a new event

To add a new event, you need to update the subscription classes with the desired logic and then bind them to the new event. This ensures that the subscription logic will be executed whenever the event is triggered.

For example, if you would like to add the car_created event to the EmailSubscription, then you should do the following:

# app/subscriptions/email_subscription.rb# frozen_string_literal: trueclass EmailSubscription < Subscription  def animal_created(payload)    puts "Email will be sent #{payload}"  end  def car_created(payload)    puts "Car email will be sent #{payload}"  endend

It's important not to forget to subscribe to this event in the initializer after adding the logic to the EmailSubscription class for the car_created event.

# config/initializers/subscriptions.rb# frozen_string_literal: trueRails.application.config.after_initialize do  subscriptions = {    EmailSubscription: ['animal_created', 'car_created'],    LoggerSubscription: ['animal_created'],    MobileNotificationSubscription: ['animal_created']  }  # ...end

When we call car_created, the subscribed logic in EmailSubscription will be triggered as expected.

ActiveSupport::Notifications.instrument('car_created', payload: {})# => Car email will be sent {:payload=>{}}

Wisper gem

The second option is to use Wisper gem.

First of all, we need to add the wisper gem to the Gemfile and then run bundle install:

gem 'wisper'

This gem has the same three key elements:

  • Send/Publish/Broadcast an event.
  • Define the logic that can potentially react to this event.
  • Bind the event with the logic ("subscribe to the event").

Send an event.

To send an event we need to do the following:

# app/units/create_animal_service.rb# frozen_string_literal: trueclass CreateAnimalService  include Wisper::Publisher  def call    # do something    broadcast(:animal_created, payload: {})  endendCreateAnimalService.new.call

Define the logic

To add logic for our events, we need to do the following:

  • EmailSubscription
# app/subscriptions/email_subscription.rb# frozen_string_literal: trueclass EmailSubscription  def animal_created(payload)    puts "Email will be sent #{payload}"  endend
  • LoggerSubscription
# app/subscriptions/logger_subscription.rb# frozen_string_literal: trueclass LoggerSubscription  def animal_created(payload)    puts "Logs will be saved #{payload}"  endend
  • MobileNotificationSubscription
# app/subscriptions/mobile_notification_subscription.rb# frozen_string_literal: trueclass MobileNotificationSubscription  def animal_created(payload)    puts "Mobile Notification will be sent #{payload}"  endend

Bind the event with the logic

To bind the event and subscription, we need to do the following

# config/initializers/subscriptions.rb# frozen_string_literal: trueRails.application.config.after_initialize do  Wisper.subscribe(EmailSubscription.new)  Wisper.subscribe(LoggerSubscription.new)  Wisper.subscribe(MobileNotificationSubscription.new)end

As you can see, the interface is quite similar to what we had in ActiveSupport::Notifications.

Dry-event

Our third option is to create a Pub/Sub system using the dry-rb gems. To be more precise, we are going to use the dry-events gem.

To get started, we need to add the dry-events gem to our Gemfile and run bundle install:

gem 'dry-events'

This gem has the same 3 key elements plus an additional one - we need to register an event. Let's take a look at the example below:

Register an event

# lib/events.rb# frozen_string_literal: truerequire 'dry/events/publisher'class Events  include Singleton  include Dry::Events::Publisher[:my_publisher]  register_event('animal.created')end

We included Singleton here because dry-event requires us to work with an instance, but we don't want to create a new instance every time.

Send an event

# app/units/create_animal_service.rb# frozen_string_literal: trueclass CreateAnimalService  def self.call    # do something    Events.instance.publish('animal.created', payload: {})  endend

Defining logic to react to the event (Subscriptions)

  • EmailSubscription
# app/subscriptions/email_subscription.rb# frozen_string_literal: trueclass EmailSubscription  def on_animal_created(event)    puts "Email will be sent #{event[:payload]}"  endend
  • LoggerSubscription
# app/subscriptions/logger_subscription.rb# frozen_string_literal: trueclass LoggerSubscription  def on_animal_created(event)    puts "Logs will be saved #{event[:payload]}"  endend
  • MobileNotificationSubscription
# app/subscriptions/mobile_notification_subscription.rb# frozen_string_literal: trueclass MobileNotificationSubscription  def on_animal_created(event)    puts "Mobile Notification will be sent #{event[:payload]}"  endend

Bind the event with the logic

# config/initializers/subscriptions.rb# frozen_string_literal: trueRails.application.config.after_initialize do  events = Events.instance  events.subscribe(EmailSubscription.new)  events.subscribe(LoggerSubscription.new)  events.subscribe(MobileNotificationSubscription.new)end

Conclusion

The final solution may look like this or be placed under the Service object which sends an event inside:

def create  animal = Animal.create(animal_params).tap { ActiveSupport::Notifications.instrument('animal_created') }  render json: animalend

In conclusion, Pub/Sub pattern is a useful design pattern in Ruby on Rails applications for managing communication between different parts of the system. The advantages of using Pub/Sub are numerous, including the most important ones:

Advantages

  • Decoupling

    One of the main advantages of the pub/sub pattern is that it allows publishers and subscribers to be decoupled from each other. Publishers do not need to know about the subscribers, and subscribers do not need to know about the publishers. This makes it easy to add or remove components from the system without affecting the other components.

  • Scalability

    The pub/sub pattern is highly scalable, since it allows multiple subscribers to receive the same message at the same time. This makes it easy to distribute messages to a large number of subscribers, without overloading the publishers or the subscribers.

  • Flexibility

    The pub/sub pattern provides a flexible way to implement communication between different components of an application, as well as between different applications. Since publishers and subscribers do not need to know about each other, it is easy to add new features or change existing ones without having to modify existing code.

  • Asynchronous

    The pub/sub pattern is asynchronous, which means that publishers and subscribers do not need to be active at the same time. Publishers can publish messages at any time, and subscribers can consume messages whenever they are ready. This makes it easy to implement real-time notifications and messaging systems.

Disadvatages

  • Complexity

    Implementing the pub/sub pattern can be complex, especially when dealing with a large number of publishers and subscribers. This pattern can add an additional layer of complexity to the system, which can make it harder to debug and maintain.

  • Reduction of application flow visibility

    Another potential disadvantage of using the Pub/Sub pattern is that it can result in a reduction of application flow visibility. This means that the overall flow of the system may be harder to understand, as the primary action and its secondary effects may be implemented in different parts of the codebase, making it difficult for future readers to comprehend the system's operation.


Original Link: https://dev.to/vladhilko/how-to-implement-pubsub-pattern-in-ruby-on-rails-1l5p

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To