Service Object в Ruby: когда `.call` уже не справляется

Service Object в Ruby on Rails — это мощный инструмент для вынесения сложной бизнес-логики из контроллеров и моделей, помогающий соблюдать принцип единой ответственности. В статье разберём, как правильно проектировать сервисные объекты, избегая типичных ошибок, и покажем их интеграцию с PostgreSQL и DevOps-практиками для масштабируемых приложений.


Итак, у нас есть CreateUser.call(params)
А потом: create_wallet, send_email, notify_crm, log_event, raise_if_duplicate.
И всё это — в одном Command. Что ж, значит пришло время для Service Object.


🧠 Теория: что такое Service Object?

Service Object — это объект, отвечающий за бизнес-процесс.
Обычно он реализует метод .call или #call, который выполняет сразу несколько связанных операций.

Он координирует действия, может вызывать команды, работать с внешними API, и при этом остаётся тестируемым и изолированным.


🔧 Пример

class Users::SignupService
  def initialize(params)
    @params = params
  end

  def call
    user = User.create!(@params)
    Wallet.create!(user: user)
    SendWelcomeEmail.call(user)
    CrmNotifier.new(user).notify_signup
    user
  end
end

Вызывается:

Users::SignupService.new(params).call

💡 Когда применять?

Service Object нужен, если:

  • Операция многосоставная: несколько шагов, зависимостей, моделей.
  • Нужна явная последовательность действий (как в скрипте).
  • Требуется разделение ответственности: контроллер → сервис → команды/API.

Он хорош в местах, где “не хочу, чтобы это было в контроллере, и не в модели тоже”.


🧪 Пример теста

describe Users::SignupService do
  it "создаёт пользователя и кошелёк" do
    params = attributes_for(:user)
    expect {
      described_class.new(params).call
    }.to change(User, :count).by(1)
      .and change(Wallet, :count).by(1)
  end
end

Можно замокать вызовы API, логеров и e-mail отправщиков — и получить предсказуемый unit-тест.


🔥 Когда это превращается в антипаттерн

Случай Почему это плохо
В сервисе 300 строк, 5 уровней вложенности Он мутировал в god-object
Сервис сам создаёт контроллер, модель, команду Нарушение инверсии зависимостей
В проекте 1000 сервисов с названиями UserCreateStep42Service Архитектура ради архитектуры

Ещё одна боль: неоднозначные namespace’ы. Где искать BanUserService — в app/services/ban/, users/, actions/?. Придётся договориться.


🎤 Что сказать на собесе

— Почему вы не пишете всю логику в моделях?

— Мы выносим сложные сценарии в сервисы: это делает код проще, уменьшает ответственность моделей, и улучшает покрытие тестами.


🧾 Вывод

Service Object — ваш рабочий верблюд для бизнес-логики. Он вытаскивает контроллеры из болота begin/rescue, модели — из жирных колбэков, а вас — с продакшена 31 декабря в 23:50.

🗓 Дата публикации: 17.09.2024, но это не точно...

Ruby on Rails Service Object бизнес-логика PostgreSQL DevOps масштабируемость