diff options
| author | Ahmed Abdelhalim <[email protected]> | 2024-03-05 07:17:29 +0200 |
|---|---|---|
| committer | Ahmed Abdelhalim <[email protected]> | 2024-03-05 07:23:13 +0200 |
| commit | 08f693548591c4cbee842125368092b8a0112a32 (patch) | |
| tree | c609d39489788aeab08c543c3a9259559101a7fb /content/blog | |
| parent | c1fcf759d48b374f54b1c7ae5381170316c26121 (diff) | |
Migrate the old pubsub blog post from toptal
Diffstat (limited to 'content/blog')
| -rw-r--r-- | content/blog/rails-pub-sub/activity-diagram.txt | 15 | ||||
| -rw-r--r-- | content/blog/rails-pub-sub/index.md | 349 |
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 |
