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

Если вы разрабатываете веб-приложения с использованием Ruby on Rails, вы, вероятно, уже знаете, что Rails — это среда MVC (Model-View-Controller), а это означает, что ваши модели отвечают за данные, представления — за шаблоны, а контроллеры — за запросы. умение обращаться.
Но чем больше становится ваше приложение, чем больше в нем функций — тем больше у вас будет бизнес-логики. И здесь возникает вопрос, где вы размещаете свою бизнес-логику? Очевидно, что это не представления, которые должны обрабатывать это. Контроллеры или модели? Это сделает их толстыми и нечитаемыми довольно скоро. Вот где сервисные объекты приходят на помощь. В этой статье мы узнаем, что такое сервисные объекты и как вы можете использовать их, чтобы сделать ваше приложение чище и поддерживать его в сопровождении.
Допустим, у вас есть проект по обработке поездок такси, мы рассмотрим конкретное действие контроллера, которое обновляет записи о поездках. Но он должен не только обновлять поездки на основе вводимых пользователем параметров (например, начальный адрес, адрес назначения, количество пассажиров и т. д.), но также должен вычислять некоторые поля на основе этих параметров и сохранять их в базе данных. Итак, у нас есть такое действие контроллера:
Проблема здесь в том, что вы добавили как минимум десять строк в свой контроллер, но этот код на самом деле не принадлежит контроллеру. Также, если вы хотите обновить поездки в другом контроллере, например, импортировав их из файла csv, вам придется повторить и переписать этот код. Или вы создаете служебный объект, то есть TripUpdateService, и используете его в любом месте, где вам нужно обновить поездки.
Что такое сервисные объекты?
По сути, сервисный объект — это обычный объект Ruby («PORO»), класс Ruby, который возвращает предсказуемый ответ и предназначен для выполнения одного единственного действия. Таким образом, он инкапсулирует часть бизнес-логики.
Задача объекта службы состоит в том, чтобы инкапсулировать функциональность, выполнять одну службу и обеспечивать единую точку отказа. Использование служебных объектов также избавляет разработчиков от необходимости писать один и тот же код снова и снова, когда он используется в разных частях приложения.
Все сервисные объекты должны иметь три вещи:
- метод инициализации
- единый публичный метод
- вернуть предсказуемый ответ после выполнения
Давайте заменим логику нашего контроллера, вызвав сервисный объект для обновлений поездки:
Выглядит намного чище, правда? Теперь давайте посмотрим, как мы реализуем сервисный объект.
Реализация объекта службы
В приложении Rails есть две папки, которые обычно используются для хранения сервисных объектов: lib/services и app/services.
По сути, вы можете выбрать то, что хотите, но в этой статье мы будем использовать app/services.
Итак, мы добавим новый класс Ruby (наш сервисный объект) в app/services/trip_update_service.rb:
Хорошо, сервисный объект добавлен, теперь вы можете вызывать TripUpdateService.new(trip, params).update_trip в любом месте вашего приложения, и оно будет работать. Rails загрузит этот объект автоматически, потому что он автоматически загружает все в папке app/.
Это уже выглядит довольно чистым, но на самом деле мы можем сделать его еще лучше. Мы можем заставить сервисный объект выполнять себя при вызове, поэтому мы можем сделать вызовы к нему еще короче. Если мы хотим повторно использовать это поведение для других сервисных объектов, мы можем добавить новый класс с именем BaseService или ApplicationService и наследовать его для нашего TripUpdateService:
Таким образом, этот метод класса с именем call создает новый экземпляр объекта службы с переданными ему аргументами или блоком, а затем вызывает метод call для этого экземпляра. Затем нам нужно сделать нашу службу наследуемой от BaseService и реализовать метод call:
Затем давайте обновим действие нашего контроллера, чтобы он правильно вызывал сервисный объект:
Где вы должны разместить свои сервисные объекты
Как мы уже обсуждали ранее, есть две базовые папки для хранения сервисных объектов: lib/services и app/services, и вы можете использовать любую из них. Другой хорошей практикой хранения ваших сервисных объектов будет их хранение в разных пространствах имен, т. е. вы можете иметь TripUpdateService, TripCreateService, TripDestroyService, SendTripService и так далее.
Но что будет общим для всех них, так это то, что они связаны с Поездками. Таким образом, мы можем поместить их в папку app/services/trips, другими словами, в пространство имен trips:
Не забудьте использовать новое пространство имен при вызове этих сервисов, то есть Trips::TripUpdateService.call(trip, params), Trips::SendTripService.call(trip, params).
Оберните свой код в одну транзакцию
Если ваш сервисный объект будет выполнять несколько обновлений для разных объектов, вам лучше заключить его в блок транзакции.
В этом случае Rails отменит транзакцию (т. е. все выполненные изменения базы данных), если какой-либо из методов объекта службы завершится ошибкой. Это хорошая практика, потому что она сохранит согласованность вашей базы данных в случае сбоя.
Это простой пример обновления нескольких записей в одной транзакции. Если какое-либо из обновлений завершается ошибкой с исключением (например, маршрут не может быть заархивирован, создание журнала изменений завершается с ошибкой), транзакция будет отменена, и база данных будет находиться в согласованном состоянии.
Передача данных в сервисные объекты и возврат ответа
По сути, вы можете передавать своим служебным объектам почти что угодно, в зависимости от операций, которые они выполняют: ActiveRecord объекты, хэши, массивы, строки, целые числа и т. д. Но вы всегда должны передавать своим служебным объектам минимальный объем данных.
Например, если вы хотите обновить поездку, вы должны передать объект поездки и хэш параметров, но не следует передавать весь хэш params, потому что он будет содержать много ненужных данных. Поэтому вы должны передавать только те данные, которые вам нужны, то есть TripUpdateService.call(trip, trip_params).
Сервисные объекты могут выполнять сложные операции. Их можно использовать для изменения записей в базе данных, отправки электронных писем, выполнения расчетов или вызова сторонних API. Так что вполне возможно, что во время этих операций что-то может пойти не так. Вот почему рекомендуется возвращать ответ от ваших сервисных объектов. Вы можете вернуть логическое значение или хэш с логическим значением и некоторыми дополнительными данными. Например, если вы хотите обновить поездку, вы можете вернуть логическое значение, указывающее, была ли поездка успешно обновлена или нет, а также вы можете вернуть сам объект поездки, чтобы вы могли использовать его в своем действии контроллера.
Однако вы должны помнить, что ваш ответ от объекта службы должен быть предсказуемым. Он всегда должен возвращать один и тот же ответ, несмотря ни на что. Поэтому, если вы возвращаете логическое значение, оно всегда должно возвращать логическое значение, а если вы возвращаете хэш, оно всегда должно возвращать хэш с теми же ключами. Это сделает ваши сервисные объекты более предсказуемыми и их будет легче тестировать.
Каковы преимущества использования сервисных объектов?
Сервисные объекты — отличный способ отделить логику вашего приложения от ваших контроллеров. Вы можете использовать их для разделения задач и повторного использования в разных частях вашего приложения. С этим шаблоном вы получаете множество преимуществ:
- Чистые контроллеры. Контроллер не должен обрабатывать бизнес-логику. Он должен отвечать только за обработку запросов и преобразование параметров запроса, сеансов и файлов cookie в аргументы, которые передаются в объект службы для выполнения действия. А затем выполните перенаправление или рендеринг в соответствии с ответом службы.
- Простое тестирование. Разделение бизнес-логики на сервисные объекты также позволяет вам независимо тестировать сервисные объекты и сонтроллеры.
- Объекты службы многократного использования. Объект службы можно вызывать из контроллеров приложений, фоновых заданий, других объектов службы и т. д. Всякий раз, когда вам нужно выполнить подобное действие, вы можете вызвать объект службы, и он выполнит работать на вас.
- Разделение ответственности. Контроллеры Rails видят только службы и взаимодействуют с объектом домена, используя их. Это уменьшение связанности упрощает масштабирование, особенно если вы хотите перейти от монолита к микросервису. Ваши службы можно легко извлечь и перенести в новую службу с минимальными изменениями.
Рекомендации по работе с сервисными объектами
- Назовите сервисные объекты rails таким образом, чтобы было понятно, что они делают. Имя сервисного объекта должно указывать на то, что он делает. В нашем примере с поездками мы можем назвать наш сервисный объект, например:
TripUpdateService,TripUpdater,ModifyTripи т. д. - Объект службы должен иметь один общедоступный метод. Другие методы должны быть закрытыми и быть доступными только внутри определенного объекта службы. Вы можете вызывать этот единственный общедоступный метод так, как хотите, просто будьте последовательны и используйте одно и то же имя для всех ваших сервисных объектов.
- Группируйте сервисные объекты по общим пространствам имен. Если у вас много сервисных объектов, вы можете сгруппировать их по общим пространствам имен. Например, если у вас много служебных объектов, связанных с поездками, вы можете сгруппировать их в пространстве имен
Trips, т. е.Trips::TripUpdateService,Trips::TripDestroyService,Trips::SendTripServiceи т. д. - Используйте синтаксический сахар для вызова ваших сервисных объектов. Используйте синтаксис proc в своем
BaseServiceилиApplicationServiceи наследуйте его в других сервисах. тогда вы можете использовать только.callв имени класса объекта службы для выполнения действия, т.е.TripUpdateService.call(trip, params) - Не забывайте спасать исключения. Когда объект службы выходит из строя из-за исключения, эти исключения следует спасать и обрабатывать должным образом. Они не должны распространяться до стека вызовов. И если исключение не может быть правильно обработано в блоке восстановления, вы должны вызвать специальное исключение, специфичное для этого конкретного объекта службы.
- Единая ответственность. Попробуйте сохранить единую ответственность за каждый из ваших сервисных объектов. Если у вас есть служебный объект, который выполняет слишком много функций, вы можете разделить его на несколько служебных объектов.
Заключение
Сервисные объекты — отличный способ отделить логику вашего приложения от ваших контроллеров. Их можно использовать для разделения проблем и их повторного использования в разных частях вашего приложения. Этот шаблон может сделать ваше приложение более тестируемым и простым в обслуживании по мере того, как вы будете добавлять все больше и больше функций.
Это также делает ваше приложение более масштабируемым и упрощает переход от монолита к микросервису. Если вы раньше не пользовались сервисными объектами, обязательно попробуйте. Кстати, Ruby on Rails используется только для этого примера, вы можете использовать тот же шаблон с другими фреймворками.
Первоначально опубликовано на https://ualeks.dev