Попробуйте что-то вроде этого:
class OrdersController < ApplicationController
def pay
# you'll need to define payment_params elsewhere in the controller
PaymentService.call(payment_params)
end
end
PaymentService
- это просто старый рубиновый объект:
class PaymentService
attr_accessor *%w(
options
).freeze
class << self
def call(options={})
new(options).call
end
end # Class Methods
#==========================================================================
# Instance Methods
#==========================================================================
def intialize(options={})
@options = options
end
def call
# do stuff
# return something
end
end
Мне лично нравится делать call
метод класса, так что мне не нужно делать:
PaymentService.new(payment_params).call
Что выглядит менее чистым для меня.Но это вопрос личных предпочтений.
Вы можете поместить его в:
app
|- ...
|- services
| |- payment_service.rb
|- ...
, что приведет к автоматической загрузке службы, и вам не придется переключаться с config.autoload_paths
.
Тестирование службытривиально (в чем смысл, не так ли?).Вот пример rspec:
require 'rails_helper'
RSpec.describe PaymentService do
before(:all) do
@method_name = "call"
end
describe "#call" do
it "responds" do
expect(described_class.respond_to?(@method_name)).to be_truthy
end
context "when using good params" do
before(:each){ @params = good_params }
it "does something" do
expect(calling_the_service).to do_something
end
end
end
end
def calling_the_service
described_class.send method_name, params
end
def good_params
{some: :arguments}
end
def params
@params
end
def method_name
@method_name
end
По правде говоря, я делаю что-то более похожее на:
class ApplicationController < ActionController::Base
# I have a custom module that lets me make this call. Among other things,
# it creates the call_service method on all controllers.
acts_as calling: :services
end
OrdersController
теперь знает, как call_service
:
class OrdersController < ApplicationController
def pay
# Given the acts_as calling: :services call, above, the OrdersController
# knows how to inspect the SERVICE_DETAIL constant on PaymentService
# and construct the appropriate arguments. In this case, passing in
# something like {current_user: 1}
call_service PaymentService
end
end
Я перемещаю некоторые вещи в ServiceBase
:
class ServiceBase
attr_accessor *%w(
options
).freeze
class << self
def call(options={})
new(options).call
end
end # Class Methods
#======================================================================
# Instance Methods
#======================================================================
def intialize(options={})
@options = options
end
private
# This method reads the REQUIRED_ARGS AND REQUIRED_VALUES constants
# and determines whether a valid service call was made. It also logs
# errors so that I can go back and see failures.
def good_to_go?
# some stuff
end
def decorated_options
@decorated_options ||= OptionsDecorator.new(options)
end
end
И теперь я объявляю некоторые метаданные службы в виде констант.По сути, они определяют интерфейс сервиса:
class PaymentService < ServiceBase
SERVICE_DETAILS = [
{current_user: [:id]}
].freeze
REQUIRED_ARGS = %w(
current_user
).freeze
REQUIRED_VALUES = %w(
current_user_id
).freeze
delegate *%w(
current_user_id
), to: :decorated_options
#======================================================================
# Instance Methods
#======================================================================
def call
raise unless good_to_go?
# do stuff
# return something
end
end