Шаблон публикации-подписки на Rails: руководство по реализации

Опубликовано: 2022-03-11

Шаблон публикации-подписки (или для краткости публикация/подписка) — это шаблон обмена сообщениями Ruby on Rails, в котором отправители сообщений (издатели) не программируют сообщения для отправки напрямую конкретным получателям (подписчикам). Вместо этого программист «публикует» сообщения (события), не зная, какие у них могут быть подписчики.

Точно так же подписчики проявляют интерес к одному или нескольким событиям и получают только те сообщения, которые представляют интерес, без ведома каких-либо издателей.

Для этого посредник, называемый «брокером сообщений» или «шиной событий», получает опубликованные сообщения, а затем пересылает их тем подписчикам, которые зарегистрированы для их получения.

Другими словами, pub-sub — это шаблон, используемый для обмена сообщениями между различными компонентами системы, при этом эти компоненты ничего не знают об идентичности друг друга.

В этом руководстве по рельсам шаблон проектирования публикации-подписки представлен на этой диаграмме.

Этот шаблон проектирования не нов, но разработчики Rails обычно не используют его. Существует множество инструментов, которые помогают включить этот шаблон проектирования в базу кода, например:

  • Wisper (который я лично предпочитаю и обсудим позже)
  • EventBus
  • EventBGBus (форк EventBus)
  • RabbitMQ
  • Редис

Все эти инструменты имеют разные базовые реализации pub-sub, но все они предлагают одни и те же основные преимущества для приложения Rails.

Преимущества реализации Pub-Sub

Уменьшение раздувания модели/контроллера

Это обычная, но не лучшая практика, когда в вашем приложении Rails есть несколько толстых моделей или контроллеров.

Шаблон pub/sub может легко помочь разложить толстые модели или контроллеры.

Меньше обратных вызовов

Наличие большого количества переплетенных обратных вызовов между моделями — это хорошо известный запах кода, который шаг за шагом тесно связывает модели вместе, что затрудняет их поддержку или расширение.

Например, модель Post может выглядеть следующим образом:

 # 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

И Post -контроллер может выглядеть примерно так:

 # 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

Как видите, у модели Post есть обратные вызовы, которые тесно связывают модель как с моделью Feed , так и с сервисом или проблемой User::NotifyFollowers . Используя любой шаблон pub/sub, предыдущий код можно преобразовать так, чтобы он выглядел примерно так, как показано ниже, в котором используется Wisper:

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

Издатели публикуют событие с объектом полезной нагрузки события, который может понадобиться.

 # 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

Подписчики подписываются только на те события, на которые они хотят ответить.

 # 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

Шина событий регистрирует различных подписчиков в системе.

 # config/initializers/wisper.rb Wisper.subscribe(FeedListener.new) Wisper.subscribe(UserListener.new)

В этом примере шаблон pub-sub полностью устранил обратные вызовы в модели Post и помог моделям работать независимо друг от друга с минимальными знаниями друг о друге, обеспечивая слабую связанность. Расширение поведения до дополнительных действий — это просто вопрос привязки к желаемому событию.

Принцип единой ответственности (SRP)

Принцип единой ответственности действительно помогает поддерживать чистоту кода. Проблема его соблюдения заключается в том, что иногда ответственность класса не так ясна, как должна быть. Это особенно распространено, когда речь идет о MVC (например, Rails).

Модели должны обрабатывать постоянство, ассоциации и многое другое.

Контроллеры должны обрабатывать запросы пользователей и быть оболочкой для бизнес-логики (сервисные объекты).

Сервисные объекты должны инкапсулировать одну из функций бизнес-логики, предоставлять точку входа для внешних сервисов или выступать в качестве альтернативы задачам модели.

Благодаря своей способности уменьшать связанность, шаблон проектирования pub-sub можно комбинировать с объектами службы с единой ответственностью (SRSO), чтобы помочь инкапсулировать бизнес-логику и предотвратить проникновение бизнес-логики в модели или контроллеры. Это сохраняет кодовую базу чистой, читаемой, ремонтопригодной и масштабируемой.

Вот пример сложной бизнес-логики, реализованной с использованием шаблона pub/sub и сервисных объектов:

Издатель

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

Подписчики

 # 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

При использовании шаблона публикации-подписки кодовая база почти автоматически организуется в SRSO. Более того, реализация кода для сложных рабочих процессов легко организуется вокруг событий, не жертвуя читабельностью, ремонтопригодностью или масштабируемостью.

Тестирование

Благодаря декомпозиции толстых моделей и контроллеров и большому количеству SRSO тестирование базы кода становится намного проще. Это особенно актуально, когда речь идет об интеграционном тестировании и межмодульном взаимодействии. Тестирование должно просто гарантировать, что события публикуются и принимаются правильно.

У Wisper есть гем для тестирования, который добавляет сопоставители RSpec для облегчения тестирования различных компонентов.

В предыдущих двух примерах (пример Post и пример Order ) тестирование должно включать следующее:

Издатели

 # 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

Подписчики

 # 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

Однако существуют некоторые ограничения на тестирование опубликованных событий, когда издатель является контролером.

Если вы хотите пройти лишнюю милю, тестирование полезной нагрузки также поможет поддерживать еще лучшую кодовую базу.

Как видите, тестирование шаблонов дизайна pub-sub очень просто. Это просто вопрос обеспечения того, чтобы различные события были правильно опубликованы и получены.

Представление

Это скорее возможное преимущество. Сам шаблон проектирования «публикация-подписка» не оказывает существенного влияния на производительность кода. Однако, как и любой инструмент, который вы используете в своем коде, инструменты для реализации pub/sub могут иметь большое влияние на производительность. Иногда это может быть плохой эффект, но иногда это может быть очень хорошо.

Во-первых, пример плохого эффекта: Redis — это «расширенный кеш и хранилище ключей-значений. Его часто называют сервером структуры данных». Этот популярный инструмент поддерживает шаблон pub/sub и очень стабилен. Однако, если он используется на удаленном сервере (не на том же сервере, на котором развернуто приложение Rails), это приведет к огромной потере производительности из-за сетевых накладных расходов.

С другой стороны, у Wisper есть различные адаптеры для асинхронной обработки событий, такие как wisper-celluloid, wisper-sidekiq и wisper-activejob. Эти инструменты поддерживают асинхронные события и многопоточное выполнение. что при правильном применении может значительно увеличить производительность приложения.

Суть

Если вы стремитесь к лишней миле в производительности, шаблон pub/sub может помочь вам достичь этого. Но даже если вы не обнаружите повышения производительности с помощью этого шаблона проектирования Rails, он все равно поможет поддерживать порядок в коде и сделает его более удобным в сопровождении. В конце концов, кто может беспокоиться о производительности кода, который невозможно поддерживать или который вообще не работает?

Недостатки реализации Pub-Sub

Как и во всем, у шаблона pub-sub также есть некоторые возможные недостатки.

Слабая связь (негибкая семантическая связь)

Самая большая из сильных сторон шаблона pub/sub является одновременно и его самой большой слабостью. Структура публикуемых данных (полезная нагрузка события) должна быть четко определена и быстро становится негибкой. Чтобы изменить структуру данных опубликованной полезной нагрузки, необходимо знать обо всех подписчиках и либо изменить их, либо убедиться, что изменения совместимы со старыми версиями. Это значительно усложняет рефакторинг кода Publisher.

Если вы хотите избежать этого, вы должны быть особенно осторожны при определении полезной нагрузки издателей. Конечно, если у вас есть отличный набор тестов, который проверяет полезную нагрузку так же, как упоминалось ранее, вам не нужно сильно беспокоиться о том, что система выйдет из строя после того, как вы измените полезную нагрузку или имя события издателя.

Стабильность шины обмена сообщениями

Издатели не знают о статусе подписчика и наоборот. С помощью простых инструментов публикации/подписки может оказаться невозможным обеспечить стабильность самой шины обмена сообщениями и обеспечить правильную постановку в очередь и доставку всех опубликованных сообщений.

Увеличение количества сообщений, которыми обмениваются, приводит к нестабильности в системе при использовании простых инструментов, и может оказаться невозможным обеспечить доставку всем абонентам без каких-то более сложных протоколов. В зависимости от количества сообщений, которыми обмениваются, и параметров производительности, которых вы хотите достичь, вы можете рассмотреть возможность использования таких сервисов, как RabbitMQ, PubNub, Pusher, CloudAMQP, IronMQ или многих других альтернатив. Эти альтернативы обеспечивают дополнительную функциональность и более стабильны, чем Wisper, для более сложных систем. Тем не менее, они также требуют некоторой дополнительной работы для реализации. Подробнее о том, как работают брокеры сообщений, можно прочитать здесь.

Бесконечные циклы событий

Когда система полностью управляется событиями, вы должны быть особенно осторожны, чтобы не было циклов событий. Эти циклы похожи на бесконечные циклы, которые могут возникать в коде. Однако их труднее обнаружить заранее, и они могут привести к остановке вашей системы. Они могут существовать без вашего уведомления, если в системе опубликовано и подписано много событий.

Учебник по Rails Заключение

Шаблон публикации-подписки не является серебряной пулей для всех ваших проблем с Rails и запахов кода, но это действительно хороший шаблон проектирования, который помогает отделить различные компоненты системы и сделать ее более удобной в сопровождении, читабельной и масштабируемой.

В сочетании с сервисными объектами с единой ответственностью (SRSO) pub-sub также может действительно помочь с инкапсуляцией бизнес-логики и предотвращением проникновения различных бизнес-задач в модели или контроллеры.

Прирост производительности после использования этого шаблона зависит в основном от используемого базового инструмента, но в некоторых случаях прирост производительности может быть значительно улучшен, и в большинстве случаев это, безусловно, не повлияет на производительность.

Однако использование шаблона pub-sub следует тщательно изучить и спланировать, потому что с большой силой слабой связи возникает большая ответственность за поддержку и рефакторинг слабо связанных компонентов.

Поскольку события могут легко выйти из-под контроля, простая библиотека публикации/подписки может не гарантировать стабильность брокера сообщений.

И, наконец, существует опасность введения бесконечных циклов событий, которые остаются незамеченными до тех пор, пока не станет слишком поздно.


Я использую этот паттерн уже почти год, и мне сложно представить написание кода без него. Для меня это клей, благодаря которому фоновые задания, служебные объекты, проблемы, контроллеры и модели взаимодействуют друг с другом четко и работают вместе как шарм.

Я надеюсь, что вы узнали столько же, сколько и я, изучив этот код, и что вы чувствуете вдохновение дать шаблону Publish-Subscribe шанс сделать ваше приложение Rails потрясающим.

Наконец, огромное спасибо @krisleech за его потрясающую работу по внедрению Wisper.