Разрешить модулю определять метод только в том случае, если включение класса/модуля не

Мне очень нравится сериализация ActiveModel, особенно запутанная паутина as_json и serializable_hash.

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

Моя команда решила, что у нас есть формат по умолчанию, которому должны следовать все эти классы при приведении к JSON (для рендеринга в приложении Rails), но некоторые из них должны вести себя немного иначе. Из-за странного поведения этих двух методов из библиотеки ActiveModel добавление атрибутов из белого или черного списка в сами модели переопределяется определением метода в этом модуле, а затем передается в супердекларации в ActiveModel.

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

Я попытался решить эту проблему, условно, динамически применяя метод включения модуля в IRB:

class A
  def foo
    puts 'in A'
  end
end

module D
  def self.included(base)
    unless base.instance_methods(false).include?(:foo)
      define_method(:foo) do
        puts 'in D'
        super()
      end
    end
  end
end

class B < A
  include D
end

class C < A
  include D
  def foo
    puts 'in C'
    super
  end
end

С этим объявлением я ожидал, что вывод C.new.foo будет

in C
in A

но это было вместо

in C
in D
in A

Моя единственная другая мысль - переместить эту логику в другой модуль и включить этот модуль в каждый класс (их около 54), который явно не переопределяет этот метод, но у этого есть пара недостатков:

  1. Это вводит немного неявной связи в проект, что новая модель включает этот модуль, если она не хочет переопределять реализацию этого метода.
  2. Текущая реализация этих методов сериализации в модуле связана с поведением и атрибутами, установленными этим модулем, поэтому я чувствую, что было бы неинтуитивно иметь второй модуль, который знает об этих деталях реализации SharedBehavior и зависит от них, хотя второй модуль не будет иметь почти ничего общего с первым.

Может ли кто-нибудь еще придумать другое решение или, может быть, заметить мою оплошность в приведенном выше примере кода, которая позволила бы мне сделать вызов в хуке included? (Я также пытался изменить порядок, в котором класс C определял метод foo и включал модуль D, но видел точно такое же поведение).


person Brad Rice    schedule 14.11.2014    source источник


Ответы (1)


Здесь есть два хитрых бага.

  1. Ruby оценивает классы, поэтому порядок выражений имеет значение. Вы include D перед определением foo в C, поэтому при вызове хука included foo не будет определено в base. Вам нужно include D в конце урока.
  2. Вы определяете foo в D. Таким образом, после включения D в B определяется D#foo, то есть он все еще включен в C, даже если вы исправите предыдущую ошибку. Вам нужно, чтобы base был получателем define_method.

Но есть интересный поворот: исправление второй ошибки делает первую ошибку неактуальной. Определив foo непосредственно в base, он будет перезаписан любыми более поздними определениями. Это было бы похоже на

class C < A
  def foo
    puts 'in D'
    super()
  end

  # overwrites previous definition!
  def foo
    puts 'in C'
    super
  end
end

Итак, чтобы подвести итог, вам просто нужно

# in D.included
base.class_eval do
  define_method(:foo) do
    puts 'in D'
    super()
  end
end
person Max    schedule 14.11.2014
comment
Отлично. На самом деле я пришел к чему-то очень похожему на это не 3 минуты назад, за исключением того, что в моем решении я вызвал частный метод define_method на base. Я пытался избегать *_eval методов, вероятно, из-за недостатка знаний с моей стороны. Я слышал, что они очень небезопасны и могут привести к угрозе безопасности. Отличается ли ваш пример в этом отношении? - person Brad Rice; 15.11.2014
comment
eval методы небезопасны только в том случае, если вы передаете строку, которая может содержать произвольный код. В этом случае все в порядке, я просто использую его для изменения неявного получателя с D на base. - person Max; 15.11.2014
comment
Сладкий. Только что проверено в IRB. Это было очень поучительное упражнение. Спасибо! - person Brad Rice; 15.11.2014