Работа над новым проектом — это весело и очень продуктивно. Возможности могут быть добавлены быстро, а идеи создания прототипов приводят к тесной петле обратной связи, идеальной для планирования 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.