summaryrefslogtreecommitdiffstats
path: root/content
diff options
context:
space:
mode:
authorAhmed Abdelhalim <[email protected]>2024-03-05 07:17:29 +0200
committerAhmed Abdelhalim <[email protected]>2024-03-05 07:23:13 +0200
commit08f693548591c4cbee842125368092b8a0112a32 (patch)
treec609d39489788aeab08c543c3a9259559101a7fb /content
parentc1fcf759d48b374f54b1c7ae5381170316c26121 (diff)
Migrate the old pubsub blog post from toptal
Diffstat (limited to 'content')
-rw-r--r--content/blog/rails-pub-sub/activity-diagram.txt15
-rw-r--r--content/blog/rails-pub-sub/index.md349
2 files changed, 364 insertions, 0 deletions
diff --git a/content/blog/rails-pub-sub/activity-diagram.txt b/content/blog/rails-pub-sub/activity-diagram.txt
new file mode 100644
index 0000000..0643da0
--- /dev/null
+++ b/content/blog/rails-pub-sub/activity-diagram.txt
@@ -0,0 +1,15 @@
+@startuml
+skinparam maxAsciiMessageLength 8
+
+participant Publisher1 as P1
+participant Publisher2 as P2
+participant MessageBroker as MB
+participant Subscriber1 as S1
+participant Subscriber2 as S2
+
+
+P1 -> MB : Message A
+P2 -> MB : Message B
+MB --> S1 : Message A
+MB --> S2 : Message B
+@enduml
diff --git a/content/blog/rails-pub-sub/index.md b/content/blog/rails-pub-sub/index.md
new file mode 100644
index 0000000..e85ed25
--- /dev/null
+++ b/content/blog/rails-pub-sub/index.md
@@ -0,0 +1,349 @@
+---
+title: Rails Pub/Sub
+date: 2014-06-21
+draft: false
+---
+The [publish-subscribe pattern][1] (or pub/sub, for short) is a messaging pattern
+where senders of messages (publishers), do not program the messages to be sent
+directly to specific receivers (subscribers).
+Instead, the programmer “publishes” messages (events), without any knowledge of
+any subscribers there may be.
+
+Similarly, subscribers express interest in one or more events, and only
+receive messages that are of interest, without any knowledge of any publishers.
+
+To accomplish this, an intermediary, called a “message broker” or “event bus”,
+receives published messages, and then forwards them on to those subscribers who
+are registered to receive them.
+
+In other words, pub-sub is a pattern used to communicate messages between
+different system components without these components knowing anything about
+each other’s identity.
+
+```ascii
+ ┌──────────┐ ┌──────────┐ ┌─────────────┐ ┌───────────┐ ┌───────────┐
+ │Publisher1│ │Publisher2│ │MessageBroker│ │Subscriber1│ │Subscriber2│
+ └────┬─────┘ └────┬─────┘ └──────┬──────┘ └─────┬─────┘ └─────┬─────┘
+ │ Message A │ │ │
+ │ ───────────────────────────>│ │ │
+ │ │ │ │ │
+ │ │ Message B │ │ │
+ │ │ ─────────────>│ │ │
+ │ │ │ │ │
+ │ │ │ Message A │ │
+ │ │ │ ─ ─ ─ ─ ─ ─ ─>│ │
+ │ │ │ │ │
+ │ │ │ Message B │
+ │ │ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ >│
+ ┌────┴─────┐ ┌────┴─────┐ ┌──────┴──────┐ ┌─────┴─────┐ ┌─────┴─────┐
+ │Publisher1│ │Publisher2│ │MessageBroker│ │Subscriber1│ │Subscriber2│
+ └──────────┘ └──────────┘ └─────────────┘ └───────────┘ └───────────┘
+ ```
+
+This design pattern is not new, and there are many tools that help incorporate
+this design pattern into your code base,
+such as:
+- [Wisper][2] (which I will use in this blog post)
+- [RabbitMQ][3]
+- [Redis][4]
+
+All of these tools have different underlying pub-sub implementations,
+but they all offer the same major advantages for a Rails application.
+
+# Advantages of Pub-Sub
+
+## Reducing Model/Controller Bloat
+
+It is a common practice, but not a best practice,
+to have some fat models or controllers in your Rails application.
+
+The pub/sub pattern can easily help [decompose fat models or controllers][5].
+
+### Fewer Callbacks
+
+Having a lot of [intertwined callbacks][6] between the models is a well-known
+known code smell, and bit by bit it tightly couples the models together,
+making them harder to maintain or extend.
+
+For example a `Post` model could look like the following:
+
+```ruby
+# 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
+```
+And the `Post` controller might look something like the following:
+
+```ruby
+# 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
+```
+As you can see, the `Post` model has callbacks that tightly couple the model
+to both the `Feed` model and the `User::NotifyFollowers` service or concern.
+By using the pub/sub pattern, the previous code could be re-factored to be something
+like the following:
+
+```ruby
+# app/models/post.rb
+class Post
+ # ...
+ field: content, type: String
+ # ...
+ # no callbacks in the models!
+end
+```
+
+**Publishers** publish the event with the event payload object that might be needed.
+```ruby
+# app/controllers/api/v1/posts_controller.rb
+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
+ # ...
+end
+```
+
+**Subscribers** only subscribe to the events they wish to respond to.
+```ruby
+# 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
+end
+```
+
+**Event Bus** registers the different subscribers in the system.
+```ruby
+# config/initializers/wisper.rb
+Wisper.subscribe(FeedListener.new)
+Wisper.subscribe(UserListener.new)
+```
+
+In this example, the pub-sub pattern completely eliminated callbacks in the
+`Post` model and helped the models to work independently from each other with
+minimum knowledge about each other, ensuring loose coupling.
+
+**Expanding the behavior to additional actions is just a matter of hooking to the desired event.**
+
+## The Single Responsibility Principle (SRP)
+
+The [Single Responsibility Principle][7] is really helpful for maintaining a clean code base.
+The problem in sticking to it is that sometimes the responsibility of the class
+is not as clear as it should be. This is especially common when it comes to MVCs (like Rails).
+
+**Models** should handle persistence, associations and not much else.
+
+**Controllers** should handle user requests and be a wrapper around the business logic (Service Objects).
+
+**[Service Objects][8]** should encapsulate one of the responsibilities of the
+business logic, provide an entry point for external services,
+or act as an alternative to model concerns.
+
+Thanks to its power to reduce coupling, the pub-sub pattern can be
+combined with single responsibility service objects (SRSOs) to help encapsulate
+the business logic, and forbid the business logic from creeping into either
+the models or the controllers. This keeps the code base clean, readable,
+maintainable and scalable.
+
+Here is an example of some business logic implemented using the pub/sub
+pattern and service objects:
+
+**Publishers**
+```ruby
+# 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
+ # ...
+```
+
+**Subscribers**
+```ruby
+# 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
+end
+```
+
+By using this pattern, the code base gets organized into SRSOs almost automatically.
+Moreover, implementing code for complex workflows is easily organized around events,
+without sacrificing readability, maintainability or scalability.
+
+## Testing
+
+By decomposing the fat models and controllers, and having a lot of SRSOs,
+testing of the code base becomes a much, much easier process.
+This is particularly the case when it comes to integration testing and
+inter-module communication. Testing should simply ensure that events are
+published and received correctly.
+
+
+Wisper has a [testing gem][9] that adds RSpec matchers to ease the testing of
+different components.
+
+**Publishers**
+```ruby
+# 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
+end
+```
+
+**Subscribers**
+```ruby
+# 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
+end
+```
+However, there are some limitations to testing the published events
+when the publisher is the controller.
+
+If you want to go the extra mile, having the payload tested as well will
+help maintain an even better code base.
+
+As you can see, pub-sub design pattern testing is simple.
+It’s just the matter of ensuring that the different events are correctly
+published and received.
+
+## Performance
+
+This is more of a *possible advantage*.
+The publish-subscribe pattern itself does not have a major inherent impact on
+code performance. However, as with any tool you use in your code, the tools
+for implementing pub/sub can have a big effect on performance.
+Sometimes it can be a bad, but sometimes it can be good.
+
+# Disadvantages of Pub-Sub Implementation
+
+## Loose Coupling (Inflexible Semantic Coupling)
+
+The greatest of the pub/sub pattern’s strengths are also it’s greatest weaknesses.
+The structure of the data published (the event payload) must be well defined,
+and quickly becomes rather inflexible. In order to modify data structure of
+the published payload, it is necessary to know about all the Subscribers,
+and either modify them also, or ensure the modifications are compatible with older
+versions. This makes refactoring of Publisher code much more difficult.
+
+If you want to avoid this you have to be extra cautious when defining the payload
+of the publishers. Of course, if you have a great test suite, that tests the
+payload as well as mentioned previously, you don’t have to worry much about the
+system going down after you change the publisher’s payload or event name.
+
+## Messaging Bus Stability
+
+Publishers have no knowledge of the status of the subscriber and vice versa.
+Using simple pub/sub tools, it might not be possible to ensure the stability
+of the messaging bus itself, and to ensure that all the published messages are
+correctly queued and delivered.
+
+The increasing number of messages being exchanged leads to instabilities in the
+system when using simple tools, and it may not be possible to ensure delivery to
+all subscribers without some more sophisticated protocols. Depending on how many
+messages are being exchanged, and the performance parameters you want to achieve,
+you might consider using services like RabbitMQ, PubNub, Pusher, CloudAMQP, IronMQ
+or many many other alternatives. These alternatives provide extra functionality,
+and are more stable than Wisper for more complex systems. However, they also
+require some extra work to implement. You can read more about how message brokers
+work [here][9]
+
+## Infinite Event Loops
+
+When the system is completely driven by events, you should be extra cautious not
+to have event loops. These loops are just like the infinite loops that can happen
+in code. However, they are harder to detect ahead of time, and they can bring
+your system to a standstill. They can exist without your notice when there are
+many events published and subscribed across the system.
+
+# Conclusion
+
+The publish-subscribe pattern is not a silver bullet for all your Rails problems
+and code smells, but it’s a really good design pattern that helps in decoupling
+different system components, and making it more maintainable, readable, and scalable.
+
+PS: a huge thank you to [@krisleech][10] for his awesome work implementing [Wisper][2].
+(used in this article)
+
+PPS: This article was originally published on: https://www.toptal.com/ruby-on-rails/the-publish-subscribe-pattern-on-rails
+
+
+
+[1]: https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern
+[2]: https://github.com/krisleech/wisper
+[3]: https://www.rabbitmq.com/tutorials/tutorial-three-ruby
+[4]: https://redis.io/docs/interact/pubsub/
+[5]: https://codeclimate.com/blog/7-ways-to-decompose-fat-activerecord-models
+[6]: https://en.wikipedia.org/wiki/Spaghetti_code
+[7]: https://en.wikipedia.org/wiki/Single_responsibility_principle
+[8]: https://web.archive.org/web/20140701062528/https://brewhouse.io/blog/2014/04/30/gourmet-service-objects.html
+[9]: https://en.wikipedia.org/wiki/Message_broker
+[10]: https://github.com/krisleech