Publish-Subcribe Pattern (hay viết tắt là pub-sub pattern) là mẫu gửi thông điệp mà người gửi (publishers), không lập trình thông điệp gửi trực tiếp tới người nhận cụ thể (subscribers). Thay vào đó, lập trình viên “gửi” các thông điệp (sự kiện), mà không hề biết gì về người nhận.

Tương tự như vậy, người nhận thể hiện sở thích vào một hay nhiều sự kiện (events), và chỉ nhận thông điệp họ mong muốn, mà không hề biết về thông tin người gửi.

Để thực hiện được điều đó, một “người trung gian” (“message brocker” or “event bus”), tiếp nhận các thông điệp, và sau đó chuyển chúng tới những người nhận mong muốn.

Nói một cách khác, pub/sub là kiểu mẫu được sử dụng để kết nối các thông điệp giữa các hệ thống khác nhau mà các hệ thống đó không cần biết thông tin cụ thể về hệ thống khác.

Học lập trình Ruby on Rails trực tuyến cơ bản đến nâng cao

Kiểu mô hình này tuy không mới, nhưng lại không được sử dụng rộng rãi trong Rails. Có một số công cụ giúp phát triển mô hình này vào code của bạn, ví dụ:

  1. Wisper
  2. EventBus
  3. EventBGBus
  4. RabbitMQ
  5. Redis

Các công cụ này có những cài đặt ngầm khác nhau, nhưng chúng đều có những thế mạnh nhất định cho ứng dụng Rails

Thế mạnh

Giảm thiểu độ phình của Model/Controller

Chúng ta thường thấy những các models và controllers phình to trong Rails app, nhưng đó không phải là cách làm tốt.

Mẫu pub/sub có thể dễ dàng giúp bạn tránh được việc này.

Giảm số lượng callbacks

Việc có nhiều callbacks giữa các models khiến cho chúng bị ràng buộc và rất khó duy trì hay phát triển.
Ví dụ một như model Post sau:

# 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

Hay một đoạn code trong Post controller có thể như sau:

# 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

Bạn có thể thấy Post model có các callbacks gắn với model FeedUser::NotifyFollowers service. Với mẫu pub/sub, đoạn code đó có thể được refactor với Wisper như sau:

# app/models/post.rb
class Post
  # ...
  field: content, type: String
  # ...
  # no callbacks in the models!
end

Publisher tạo event

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

Subscribers chỉ tiếp nhận events cần thiết

# 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 đăng kí tạo các subscribers khác nhau trong hệ thống.

# config/initializers/wisper.rb

Wisper.subscribe(FeedListener.new)
Wisper.subscribe(UserListener.new)

Trong ví dụ này, mẫu pub/sub hoàn toàn loại bỏ các callbacks trong Post model và giúp các models hoạt động hoàn toàn độc lập với những model khác. Việc mở rộng ra các hành vi khác chỉ là vấn đề liên kết với event mong muốn.

Nguyên lý đơn nhiệm

Nguyên lý đơn nhiệm thực sự rất hữu ích cho việc phát triển một code ‘sạch’. Nhưng có một vấn đề là đôi khi các lớp chúng ta tạo ra lại không có một vai trò rõ ràng. Điều này là dễ gặp với các MVCs frameworks, (ví dụ như Rails).

 

Models chỉ nên quản lý persistence các associations, không nên bao gồm những thứ khác.
Controllers chỉ nên quản lý các yêu cầu từ phía người dùng, đóng gói business logic (Service Objects).

Service Objects nên đóng gói các chức năng của business logic, cung cấp cách thức truy cập cho các services bên ngoài, hoặc đóng vai trò như một thay thế với những gì model quan tâm tới.

Pub/Sub pattern có thể tạo ra các single responsibility service object để đóng gói business logic, và ngăn cản được việc business logic lấn sang phần sân của models hay controllers. Điều này giúp cho code được sạch sẽ, dễ đọc, dễ maintain, và dễ dàng scale up.

Dưới đây là một ví dụ:

Publisher

# 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

# 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

Performance

Pub/Sub pattern có thể có ảnh hưởng tích cự, hoặc tiêu cực tới performance, tùy theo cách bạn sử dụng.

Wisper có rất nhiều adapters hỗ trợ xử lý các sự kiện không đồng bộ và thực thi nhiều threads, điều này có thể giúp bạn cải thiện đáng kể về performance.

Tuy nhiên, Nếu bạn xử lý dữ liệu ở một server remote, việc sử dụng nhiều pub/sub có thể dẫn tới hệ quả không tốt về performance, vì nó phải tải qua network.

Source:
http://www.toptal.com/