Эта статья была изменена по сравнению с ее первоначальным видом в Playbook Thirty NineA Guide to Shipping Interactive Web Apps with Minimal Tooling, и адаптирована в соответствии с этой гостевой публикацией для AppSignal. эм>

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

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

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

Сервисный объект — это просто обычный объект Ruby («PORO»). Это просто файл, который находится в определенном каталоге. Это класс Ruby, который возвращает предсказуемый ответ. То, что делает ответ предсказуемым, связано с тремя ключевыми частями. Все сервисные объекты должны следовать одному и тому же шаблону.

  • Имеет метод инициализации с аргументом params.
  • Имеет один общедоступный метод с именем call.
  • Возвращает OpenStruct с успехом? и либо полезная нагрузка, либо ошибка.

Что такое OpenStruct?

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

Если успех true, он возвращает полезную нагрузку данных.

OpenStruct.new({success ?:true, payload: 'some-data'})

Если успех равен false, возвращается ошибка.

OpenStruct.new({success ?:false, error: 'some-error'})

Вот пример сервисного объекта, который получает данные из нового API AppSignals, который в настоящее время находится в стадии бета-тестирования.

Вы бы назвали файл выше с помощью AppServices::AppSignalApiService.new({endpoint: 'markers'}).call. Я свободно использую OpenStruct, чтобы получить предсказуемый ответ. Это действительно ценно, когда дело доходит до написания тестов, потому что все архитектурные шаблоны логики идентичны.

Что такое модуль?

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

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

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

Например, каталог CloudflareServices содержит определенные сервисные объекты для создания и удаления субдоменов в Cloudflare. Службы Wistia и Zapier содержат соответствующие служебные файлы.

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

Давайте покопаемся в каталоге StripeServices. В этом каталоге хранятся отдельные сервисные объекты для взаимодействия с Stripes API. Опять же, единственное, что делают эти файлы, — это берут данные из нашего приложения и отправляют их в Stripe. Если вам когда-нибудь понадобится обновить вызов API в объекте StripeService, который создает подписку, у вас есть только одно место для этого.

Вся логика, которая собирает данные для отправки, выполняется в отдельном сервисном объекте, расположенном в каталоге AppServices. Эти файлы собирают данные из нашего приложения и отправляют их в соответствующий каталог службы для взаимодействия с внешним API.

Вот наглядный пример: предположим, что у нас есть кто-то, кто начинает новую подписку. Все происходит от контроллера. Вот SubscriptionsController.

Сначала мы создадим подписку в приложении, и если она будет успешной, мы отправим ее, stripeToken и прочее, например купон, в файл с именем AppServices::SubscriptionService.

В файле AppServices::SubscriptionService должно произойти несколько вещей. Вот этот объект, прежде чем мы перейдем к тому, что происходит:

Из обзора высокого уровня, вот что мы смотрим:

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

  1. Мы проверяем, сохранен ли stripe_customer_id в профиле пользователя. Если это так, мы извлекаем клиента из Stripe только для того, чтобы убедиться, что клиент действительно существует, а затем возвращаем его в полезной нагрузке нашего OpenStruct.
  2. Если клиент не существует, мы создаем клиента, сохраняем stripe_customer_id, а затем возвращаем его в полезной нагрузке OpenStruct.

В любом случае наш CustomerService возвращает идентификатор клиента Stripe, и он сделает все необходимое, чтобы это произошло. Вот этот файл:

Надеюсь, вы начинаете понимать, почему мы структурируем нашу логику по множеству сервисных объектов. Могли бы вы представить себе одного гигантского гигантского файла со всей этой логикой? Ни за что!

Вернемся к нашему файлу AppServices::SubscriptionService. Теперь у нас есть клиент, которого мы можем отправить в Stripe, который дополняет данные, необходимые нам для создания подписки на Stripe.

Теперь мы готовы вызвать последний объект службы, файл StripeServices::CreateSubscription.

Опять же, сервисный объект StripeServices::CreateSubscription никогда не меняется. У него есть единственная обязанность: принимать данные, отправлять их в Stripe и либо возвращать успех, либо возвращать объект в качестве полезной нагрузки.

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

Вот где все становится интересно. Мы используем Mavenseed в качестве примера, хотя та же логика работает и в SportKeeper. Наше мультитенантное приложение представляет собой единый монолит, разделяющий таблицы, разделенные столбцом site_id. Каждый арендатор подключается к Stripe через Stripe Connect, после чего мы получаем идентификатор учетной записи Stripe для сохранения в учетной записи арендатора.

Используя те же вызовы API Stripe, мы можем просто передать учетную запись Stripe подключенной учетной записи, и Stripe выполнит вызов API от имени подключенной учетной записи.

Таким образом, наш объект StripeService выполняет двойную функцию вместе с основным приложением и арендаторами, вызывая один и тот же файл, но отправляя разные данные.

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

Во-первых, в методе «call» есть инструкции «rescue» и «else». Это то же самое, что написать следующее:

Но методы Ruby автоматически начинают блок неявно, поэтому нет необходимости добавлять начало и конец. Это утверждение читается как «создать подписку, вернуть ошибку, если она есть, в противном случае вернуть подписку».

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

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

P.S. Если вы хотите читать сообщения о Ruby Magic, как только они выходят из печати, подпишитесь на нашу рассылку новостей о Ruby Magic и никогда не пропустите ни одного сообщения!

Прочтите эту и другие главы, прочитав мою новую книгу Playbook Thirty NineРуководство по доставке интерактивных веб-приложений с минимальным набором инструментов. В этой книге я применяю нисходящий подход к описанию распространенных шаблонов и методов, основываясь исключительно на своем личном опыте индивидуального разработчика, создающего и поддерживающего несколько веб-приложений с высоким трафиком и высоким доходом.

Используйте код купона appsignalrocks и сэкономьте 30%!

Первоначально опубликовано на https://blog.appsignal.com 17 июня 2020 г.