2016-08-02 4 views
0

Для примера у меня есть класс с двумя методами:Динамический метод вызова с аргументами

class Example < ActiveRecord::Base 
    def method_one(value) 

    end 
    def method_two 

    end 
end 

и метод в контроллере, где я называю их:

def example 
    ex = Example.find(params[:id]) 
    ex.send(params[:method], params[:value]) if ex.respond_to?(params[:method]) 
    end 

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

ArgumentError (wrong number of arguments (1 for 0)) 

Это происходит потому, что params[:value] возвращается nil. Самое простое решение:

def example 
    ex = Example.find(params[:id]) 
    if ex.respond_to?(params[:method]) 
     if params[:value].present? 
     ex.send(params[:method], params[:value]) 
     else 
     ex.send(params[:method]) 
     end 
    end 
    end 

Интересно, есть ли какие-либо лучше обойти, чтобы не передать аргумент, если это нуль.

ответ

2

То, что вы пытаетесь сделать, может быть очень опасно, поэтому я рекомендую вам фильтровать params[:method] раньше.

allowed_methods = { 
    method_one: ->(ex){ex.method_one(params[:value])} 
    method_two: ->(ex){ex.method_two} 
} 
allowed_methods[params[:method]]&.call(ex) 

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

Я получаю только лямбда, если params[:method] находится в хеше allowed_methods.

&. синтаксиса является новым безопасным оператором навигации в рубине 2.3, и - для краткости - выполняет следующий метод, если приемник не ноль (т.е. результата allowed_methods[params[:method]]) Если вы не используете рубиновый> = 2.3 , вы можете использовать вместо try, которые имеют подобное поведение в этом случае:

allowed_methods[params[:method]].try(:call, ex) 

Если вы не фильтровать значение params[:method], то пользователь может просто передать :destroy, например, чтобы удалить запись, которая конечно, не то, что вы хотите.

Также, позвонив по номеру ex.send ..., вы обходите инкапсуляцию объекта, которой обычно не требуется.Чтобы использовать только общедоступный интерфейс, предпочитайте использовать public_send.


Другая точка на большой изъян безопасности вас код:

eval является частным методом, определенным на Object (фактически унаследованной от Kernel), так что вы можете использовать его таким образом на любом объекте:

object = Object.new 
object.send(:eval, '1+1') #=> 2 

Теперь, с кодом, представьте пользователь помещает eval как значение params[:method] и произвольный код рубина в params[:value], он может на самом деле whateve он хочет в вашей заявке.

+0

Я думал о безопасности, но я не знал случая с 'eval'. Передача таких методов, как 'update',' destroy' и т. Д., Для меня не была большой проблемой, потому что это была одна из вещей, которые я хотел разрешить пользователю. У меня есть один вопрос. Почему бы вам не замораживать хэш с allow_methods? – Gregy

+0

Я рекомендую использовать что-то похожее на то, что я впервые написал в своем ответе, он будет более безопасным и обрабатывает по каждому методу количество параметров. – Geoffroy

+0

'allowed_methods' определенно хорошая идея, но реализация является прекрасным примером чрезмерного дизайна. '% i | method_one method_two |' достаточно. Трюк с использованием переноса с одинаковым количеством параметров - очень плохая идея: он нарушает принцип SRP и в основном делает этот код неприемлемым. – mudasobwa

1

Если вы понимаете, что вы делаете, есть более простые обходные пути:

def method_two _ = nil 
end 

или

def method_two * 
end 

Он работает, а наоборот:

def method_one *args 
end 
def method_two * 
end 

и:

ex.public_send(params[:method], *[params[:value]]) \ 
    if ex.respond_to?(params[:method]) 

Sidenote: предпочитает public_send над send, если вы явно не вызываете метод private.


Использование splatted Params без изменения сигнатуры методов:

ex.public_send(*[params[:method], params[:value]].compact) 
+0

Я бы хотел избежать методов модификации. Можете ли вы объяснить, что делает оба дополнения? – Gregy

+1

Все фрагменты выше, за исключением первого, используйте [splatted params] (https://endofline.wordpress.com/2011/01/21/the-strange-ruby-splat/). Если вы хотите вызывать разные методы с разными явными сигнатурами, вы должны выполнить проверку или выполнить сложный 'ex.public_send (* [params [: method], params [: value]]. Compact)'. Последний удалит параметр «nil» на месте. – mudasobwa