Статьи

Использование Wisper для разложения приложений

Снимок экрана 2013-06-18 в 5.39.22 вечера Работа над новым проектом — это весело и очень продуктивно. Возможности могут быть добавлены быстро, а идеи создания прототипов приводят к тесной петле обратной связи, идеальной для планирования Agile / Lean-esque.

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

Преобладающий голос в сообществе Rails защищает так называемый «путь Rails». Это золотой предписанный путь, причина, по которой многие из нас, без сомнения, вышли на Rails в первую очередь. В этом прелесть Rails и его прославленные повышения производительности.

Но почему-то эти достижения казались разочаровывающе короткими.

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

Вдохновение

Впервые я услышал о гексагональных рельсах в выступлении Мэтта Уинна на GoRuGo 2012. Я сразу же узнал проблемы, которые он описал в моем собственном коде, частично контроллеры, написанные в процедурном стиле, изобилующие операторами if . Мэтт показывает, как преодолеть это, используя шаблон pub-sub, что я и буду демонстрировать в этой статье.

Идея, лежащая в основе Hexagonal Rails, заключается в том, чтобы отделить основное приложение от механизма доставки, что было отражено в основной лекции Роберта Мартина «Архитектура потерянных лет» и « Авди Гримм».

Идея на вынос — ваше приложение не является приложением Rails. Rails — это не что иное, как фреймворк, используемый для упаковки вашего приложения в протокол HTTP. Это деталь, и мы должны заниматься объектно-ориентированной разработкой, а не Rails-ориентированной разработкой.

Еще один клон Ebay

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

В начале история очень проста; создайте новую ставку и сообщите пользователю, была ли она успешной или нет. Легко, используйте «Rails Way», доставляйте и отправляйте. Agile-ки-ад огня.

Через некоторое время в журнал добавляются новые истории:

  • Отправить продавцу товар по электронной почте
  • Обновите интерфейс наблюдателей предметов в реальном времени
  • Сохранить статистику
  • Обновление каналов активности для всех подписчиков элемента

Код для этих новых историй должен касаться нескольких моделей и использовать внешний API. Куда бы он ни шел (модель, наблюдатель или контролер), он будет вводить зависимости, которые не являются основными задачами «назначения цены на товар». Тестирование и рефакторинг станут трудными, а производительность замедлится.

Куда еще мог пойти этот код? У нас уже есть слова: «создать ставку» или CreateBid . Похоже, это был бы хороший кандидат на новый объект, который моделирует создание ставки.

Этот тип объекта часто упоминается как сервис, сценарий использования или команда. Я буду использовать слово «сервис».

Сервисы обычно используются для организации взаимодействия между несколькими моделями и / или взаимодействия с внешними системами (например, HTTP и SMTP).

Сервисный объект кажется хорошей идеей, но разве мы не переместили проблему?

Разве наши сервисы не станут раздутыми как модели? Да!

Wispered Service Objects

Wisper — это библиотека, которая дополняет методы и атрибуты Ruby событиями.

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

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

Контроллер:

 class BidsController def new @bid = Bid.new end def create service = CreateBid.new service.subscribe(WebsocketListener.new) service.subscribe(ActivityListener.new) service.subscribe(StatisticsListener.new) service.subscribe(IndexingListener.new) service.on(:reserve_item_successfull) { |bid| redirect_to item_path(bid.item) } service.on(:reserve_item_failed) { |bid| @bid = Bid.new(bid_params); render :action => 'new' } service.execute(current_user, bid_params) end private def bid_params params[:bid] end end 

Сервис:

 class CreateBid include Wisper::Publisher def execute(performer, attributes) bid = Bid.new(attributes) bid.user = performer if bid.valid? bid.save notify_seller_of(bid) broadcast(:bid_successful, performer, bid) else broadcast(:bid_failed, performer, bid) end end private def notify_seller_of(bid) BidMailer.new_bid(bid).deliver end end 

Некоторые слушатели:

 class WebsocketListener def bid_successful(performer, bid) Pusher.trigger("item_#{bid.item_id}", 'new_bid', :amount => bid.amount) end end class ActivityListener def reserver_item_successful(performer, bid) # ... end end 

Первое, что вы можете заметить, это то, что контроллер не имеет if , очень удобочитаем и имеет приятную форму.

Мы переместили вспомогательные задачи (уведомления веб-сокетов, канал активности) в отдельные объекты.

Контроллер (контекст) делает следующее:

  • Создает сервисный объект
  • Подключает любых слушателей, которые должны знать об ответе
  • Выполняет службу, передающую данные, извлеченные из HTTP-запроса.

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

Если нам нужно добавить новую функцию в процессе регистрации, у нас есть два варианта:

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

Публикация событий

Просто Wisper::Publisher в любой объект, чтобы позволить ему публиковать события.

 class MyPublisher include Wisper::Publisher def do_something publish(:done_something, 'hello', 'world') end end 

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

Когда publish вызывается done_something будет вызываться для всех слушателей, которые имеют ( respond_to? ) Этот метод. Метод должен иметь подпись аргумента, которая может принимать публикуемые аргументы, поэтому в приведенном выше примере слушатель должен иметь:

 def done_something(greeting, location) # ... end 

Метод publish также называется псевдонимом, как broadcast и emit .

Подписка на события

 my_publisher = MyPublisher.new my_publisher.subscribe(MyListener.new) 

Мы также можем при желании ограничить, какие события отправляются слушателю:

 create_bid.subscribe(WebsocketListener.new, :on => :bid_successful) 

Вы также можете передать массив, если вам нужно определенное несколько событий.

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

 create_bid.subscribe(BidListener.new, on => :bid_successful, :with => :successful) 

Мы также можем подписать блок, это используется в контроллерах, чтобы позволить им подписаться на события и отвечать правильно. Вы можете разумно добавить контроллер в качестве прослушивателя, например, create_bid.subscribe(self) , но добавление блока делает его более create_bid.subscribe(self) Rails.

 create_bid.on(:reserve_item_successfull) do |reservation| redirect_to reservation_path(reservation) end 

Глобальные Слушатели

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

В приложении Rails я обычно добавляю глобальные слушатели в инициализаторе.

 Wisper::GlobalListeners.add_listener(StatisticsListener.new) 

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

В итоге

Комбинируя объекты Service с Wisper, мы получаем следующие преимущества:

  • Меньшие объекты с меньшим количеством обязанностей
  • Объекты могут быть подключены на основе контекста в легкой форме
  • Основное применение не загрязнено проблемами механизма доставки
  • Объекты говорят слушателям, что произошло, а не что делать (доверяя, не контролируя)
  • Изолированное тестирование проще и потому что мы можем избавиться от необходимости, чтобы Rails-тесты выполнялись быстрее.
  • Четкие границы существуют между объектами в разных слоях.
  • Переход в асинхронный режим проще, так как мы можем выполнить из фонового задания без загрузки Rails

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

Как и при любом таком рассмотрении, один размер подходит не всем. Лично я не использую Wisper с простыми приложениями CRUD. Но если (когда!) Приложение CRUD будет развиваться за пределами области Rails, тогда я начну использовать сервисные объекты Wispered.

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

Рефакторинг зрелых кодовых баз

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

Разрабатывая Wisper, я попробовал много методов для реализации Pub-Sub в своем коде, и он все еще содержит некоторые из этих экспериментов. Я заменяю их только на Wisper, когда мне нужно прикоснуться к соответствующему контроллеру по какой-то другой причине.

Как только модель или сервисный объект имеют Wisper, добавить новых слушателей очень просто. Например, кэширование может быть перемещено из модели / наблюдателя / контроллера и в слушатель.

Вы даже можете применить Wisper к модели ActiveRecord так же легко, как и любой другой объект, если хотите удалить некоторые after_ вызовы after_ .

Попробуйте, найдите одно место в вашем Rails-приложении, которому нужен TLC, и создайте ветку. Посмотрите, получится ли это к лучшему.

Я хотел бы услышать, как вы поживаете, ваши мысли и отзывы о Github, в комментариях или в Twitter.