параметр конструктора контроллера в рельсах? - PullRequest
0 голосов
/ 08 июня 2018

Как мы можем внедрить параметр в конструктор контроллера или какова философия внедрения зависимостей в рельсы?

Допустим, мне нужно внедрить платежный сервис в контроллер.Из мира .NET я бы использовал DI-фреймворк, настроив его для внедрения экземпляра CreditPaymentService для зависимостей IPaymentService.

В рельсах, как мы можем этого достичь?Специально для тестирования и макета?

Я прочитал несколько источников, утверждая, что нам не нужна DI-инфраструктура для выполнения DI в ruby, но я не понимаю, как это сделать для контроллера?

Спасибо!

// .NET
public class OrderController : Controller
{
    IPaymentService _paymentService;

    public OrderController(IPaymentService paymentService)
    {
        _paymentService = paymentService;
    }

    public object Pay(Order order)
    {       
        _paymentService.process(order.GetTotal());
    }
}

# Rails
class order_controller < ApplicationController

    def initialize(payment_service)
        @payment_service = payment_service
    end 

    def pay
        order = request.parameters(:order)
        @payment_service.process(order)
    end
end

Ответы [ 2 ]

0 голосов
/ 08 июня 2018

В Rails добавление конструктора к контроллеру не имеет смысла.Если вы хотите «внедрить» некоторую функциональность в контроллер, вы можете определить модуль mixin (например, в каталоге lib) и include его в контроллере

module Foo
  def say_hello!
    puts "Hello world"
  end
end

class OrdersController < ApplicationController
  include Foo

  def index
    say_hello! # "Hello world"
  end
end

или определить его каккласс обслуживания и вызовите его напрямую

class FooService
  def initialize
    puts "Hello world"
  end
end

class OrdersController < ApplicationController
  def index
    FooService.new # "Hello world"
  end
end

Имейте в виду, что файлы в lib не загружаются Rails по умолчанию, и вам нужно сделать что-то вроде

config.autoload_paths += %W(#{config.root}/lib)

в application.rb

0 голосов
/ 08 июня 2018

Попробуйте что-то вроде этого:

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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...