Расширение собственного рубинового класса для загрузки CarrierWave
Обновление: я упростил свой вопрос; Вы можете увидеть полную историю, проверив мои правки редактирования. Спасибо iain и BernardK за то, что они так далеко BernardK.
Я хочу загрузить функциональность carrierwave в экземпляр моего User < ActiveRecord::Base
модель.
require 'uploaders/avatar_uploader'
module HasAnAvatar
def self.extended(host)
if host.class == Class
mount_uploader :avatar, AvatarUploader
else
class << host
mount_uploader :avatar, AvatarUploader
end
end
end
end
Выполнение:
(user = User.first).extend(HasAnAvatar).avatar
Результаты в:
NoMethodError: неопределенный метод
new' for nil:NilClass from /Users/evan/.rbenv/versions/1.9.3-p286/lib/ruby/gems/1.9.1/gems/carrierwave-0.6.2/lib/carrierwave/mount.rb:306:in
загрузчик"
Я подозреваю, что проблема в том, что mount_uploader
в HasAnAvatar
не вызывается должным образом на собственный класс для user
так, что uploaders
хэш не заполнен.
Любые мысли о том, как заставить это работать?
Вот пример приложения Rails для этой проблемы: https://github.com/neezer/extend_with_avatar_example
5 ответов
Хорошо, я думаю, что выяснил, что стало причиной моих проблем...
В https://github.com/jnicklas/carrierwave/blob/master/lib/carrierwave/mount.rb, в определении CarrierWave::Mount::Mounter
Есть три ссылки на record.class
, Это справедливо относится к основному User
класс, который не имеет расширенных методов, которые я загрузил в метакласс пользователя. Итак, я изменил их на это: https://gist.github.com/4465172, и это, похоже, работает.
Также, кажется, продолжает работать, если используется обычно, как в документах CarrierWave, так что это тоже хорошо. Будем продолжать его тестировать.
Вот два способа, которыми вы можете включить модуль в экземпляр (который должен в основном расширить собственный класс экземпляра). Я не думаю, что это лучший ответ на вашу проблему, даже если он может ответить на вопрос (частично).
class A
end
# => nil
module B
def blah
"Blah!"
end
end
# => nil
a = A.new
=> #<A:0x0000010086cdf0>
a.blah
# NoMethodError: undefined method `blah' for #<A:0x0000010086cdf0>
class << a
include B
end
a.blah
# => "Blah!"
b = A.new
# => #<A:0x0000010083b818>
b.blah
# NoMethodError: undefined method `blah' for #<A:0x0000010083b818>
b.extend B
# => #<A:0x0000010083b818>
b.blah
# => "Blah!"
c.blah
# NoMethodError: undefined method `blah' for #<A:0x0000010085eed0>
Из вашего редактирования:
module Pacifiable
def pacified_with(mechanism)
class_eval do
define_method(:"pacified_with_#{mechanism}?") { true }
end
end
end
# => nil
class JellyFish
define_method(:is_squishy?) { true }
end
# => #<Proc:0x00000100850448@(irb):10 (lambda)>
class Lobster
extend Pacifiable
pacified_with :polar_bear
define_method(:is_squishy?) { false }
end
# => #<Proc:0x00000100960540@(irb):16 (lambda)>
lobster = Lobster.new
# => #<Lobster:0x0000010095aa50>
lobster.pacified_with_polar_bear?
# => true
jelly = JellyFish.new
# => #<JellyFish:0x00000100951108>
jelly.pacified_with_polar_bear?
# NoMethodError: undefined method `pacified_with_polar_bear?' for #<JellyFish:0x00000100951108>
class << jelly
extend Pacifiable
pacified_with :polar_bear
end
# => #<Proc:0x0000010093ddd8@(irb):4 (lambda)>
jelly.pacified_with_polar_bear?
# => true
big_jelly = JellyFish.new
# => #<JellyFish:0x0000010091ad38>
big_jelly.pacified_with_polar_bear?
# NoMethodError: undefined method `pacified_with_polar_bear?' for #<JellyFish:0x0000010091ad38>
Из того, что я знаю о классах Ruby, когда я включаю модуль в класс,... но не изменяю задним числом любые существующие экземпляры User.
Напротив, включение / расширение немедленно влияет на все существующие экземпляры, поскольку речь идет о указателе между классом и его суперклассом. Посмотрите, как наследование работает в Ruby? а также ссылки внутри.
module HasAnAvatar
def m
puts 'in HasAnAvatar#m'
end
end
class AvatarUploader; end
class User
end
user = User.new
print 'user.respond_to?(:m) ? '; puts user.respond_to?(:m) ? 'yes' : 'no'
print '1) user.respond_to?(:avatar) ? '; puts user.respond_to?(:avatar) ? 'yes' : 'no'
class User
include HasAnAvatar
end
print 'user.respond_to?(:m) ? '; puts user.respond_to?(:m) ? 'yes' : 'no'
print '2) user.respond_to?(:avatar) ? '; puts user.respond_to?(:avatar) ? 'yes' : 'no'
module HasAnAvatar
def self.included(base)
puts "#{self} included into #{base}"
# base.mount_uploader :avatar, AvatarUploader
base.send(:attr_reader, :avatar)
end
end
class User
include HasAnAvatar
end
print '3) user.respond_to?(:avatar) ? '; puts user.respond_to?(:avatar) ? 'yes' : 'no'
print 'user.avatar : '; p user.avatar
print 'user.avatar.class : '; p user.avatar.class
user.instance_variable_set(:@avatar, AvatarUploader.new)
print 'after set, user.avatar : '; p user.avatar
print 'user.avatar.class : '; p user.avatar.class
Исполнение (Ruby 1.9.2):
$ ruby -w t.rb
user.respond_to?(:m) ? no
1) user.respond_to?(:avatar) ? no
user.respond_to?(:m) ? yes
2) user.respond_to?(:avatar) ? no
HasAnAvatar included into User
3) user.respond_to?(:avatar) ? yes
user.avatar : nil
user.avatar.class : NilClass
after set, user.avatar : #<AvatarUploader:0x007fcc2b047cf8>
user.avatar.class : AvatarUploader
Таким образом, включенные методы сразу становятся доступными для всех существующих экземпляров.
Почему user.avatar
ответь ноль? Потому что переменные экземпляра принадлежат... отдельным экземплярам. См. Почему символы в Ruby не считаются типом переменной? В этом случае старому пользователю не был назначен аватар.
Но почему вы получаете user2.avatar.class #=> AvatarUploader
, Я полагаю, что за сценой, base.mount_uploader :avatar, AvatarUploader
делает что-то такое, что:avatar не является аксессором соответствующей переменной экземпляра или определяет метод инициализации, который начинает устанавливать эту переменную в новые экземпляры.
Вот решение для второго примера:
module Pacifiable
def self.extended(host)
puts "#{host} extended by #{self}"
def host.pacified_with(mechanism)
@@method_name = "pacified_with_#{mechanism}?"
puts "about to define '#{@@method_name}' into #{self} of class #{self.class }"
if self.class == Class
then # define an instance method in a class
define_method(@@method_name) { true }
else # define a singleton method for an object
class << self
define_method(@@method_name) { true }
end
end
end
end
end
class JellyFish
define_method(:is_squishy?) { true }
end
class Lobster
extend Pacifiable
pacified_with :polar_bear
define_method(:is_squishy?) { false }
end
a_lobster = Lobster.new
print 'a_lobster.pacified_with_polar_bear? '; p a_lobster.pacified_with_polar_bear?
print 'a_lobster.is_squishy? '; p a_lobster.is_squishy?
jelly = JellyFish.new
### Add functionality to instance
#
## what I want:
#
jelly.extend(Pacifiable)
jelly.pacified_with(:polar_bear)
print 'jelly.pacified_with_polar_bear? '; p jelly.pacified_with_polar_bear? #=> true
Исполнение:
Lobster extended by Pacifiable
about to define 'pacified_with_polar_bear?' into Lobster of class Class
a_lobster.pacified_with_polar_bear? true
a_lobster.is_squishy? false
#<JellyFish:0x007fcc2b047640> extended by Pacifiable
about to define 'pacified_with_polar_bear?' into #<JellyFish:0x007fcc2b047640> of class JellyFish
jelly.pacified_with_polar_bear? true
Около jelly.class.pacified_with(:polar_bear)
: ошибка была в том, чтобы позвонить class
, который отвечает классу JellyFish
; то, что вы хотите, это одноэлементный класс объекта jelly. Как только метод определен в одноэлементном классе объекта, вы можете отправить его непосредственно объекту. То же самое относится к классам, которые являются экземплярами класса. Как только метод определен в одноэлементном классе класса, вы можете отправить его непосредственно в класс. Мы называем их методами класса, они на самом деле являются методами экземпляра синглтон-класса класса. Оуф!
Последнее ИЛИ: как объяснено, если вы расширяете класс, он действителен для всех существующих экземпляров. Таким образом, вы должны расширить отдельные экземпляры:
class JellyFromTheBigBlueSea
def find
puts 'in JellyFromTheBigBlueSea#find'
jelly = JellyFish.new
jelly.extend(Pacifiable)
jelly.pacified_with :polar_bear
jelly
end
end
class JellyFromAnIsolatedCove
def find
puts 'in JellyFromAnIsolatedCove#find'
JellyFish.new
end
end
normal_jelly = JellyFromTheBigBlueSea.new.find
ignorant_jelly = JellyFromAnIsolatedCove.new.find
## what I want:
#
print 'normal_jelly.pacified_with_polar_bear? '; p normal_jelly.pacified_with_polar_bear?
print 'ignorant_jelly.pacified_with_polar_bear?' ; p ignorant_jelly.pacified_with_polar_bear?
Исполнение:
in JellyFromTheBigBlueSea#find
#<JellyFish:0x007fb5d9045060> extended by Pacifiable
about to define 'pacified_with_polar_bear?' into #<JellyFish:0x007fb5d9045060> of class JellyFish
in JellyFromAnIsolatedCove#find
normal_jelly.pacified_with_polar_bear? true
ignorant_jelly.pacified_with_polar_bear?t.rb:109:in `<main>': undefined method `pacified_with_polar_bear?' for #<JellyFish:0x007fb5d9044d18> (NoMethodError)
Прежде всего, путь с экземпляром
extend
звонки работает (см. конец поста).- Тогда вы должны принять во внимание некоторые мнения, что это плохо для производительности
- И наконец, по словам источников, все должно работать как положено. Поэтому я предлагаю вам надеть шляпу отладки и попытаться увидеть, что именно происходит на
user.extend
часть. Например, посмотрите, еслиmount_uploader
использованияCarrierwave::Mount#mount_uploader
метод, как есть определенные Everrides Uploader.
1.9.3p327 :001 > class A
1.9.3p327 :002?> def foo
1.9.3p327 :003?> '42'
1.9.3p327 :004?> end
1.9.3p327 :005?> end
=> nil
1.9.3p327 :006 > A.new.foo
=> "42"
1.9.3p327 :011 > module Ext
1.9.3p327 :012?> def foo
1.9.3p327 :013?> 'ext'
1.9.3p327 :014?> end
1.9.3p327 :015?> end
=> nil
1.9.3p327 :016 > class AFancy
1.9.3p327 :017?> def call
1.9.3p327 :018?> a = A.new
1.9.3p327 :019?> a.extend Ext
1.9.3p327 :020?> a
1.9.3p327 :021?> end
1.9.3p327 :022?> end
=> nil
1.9.3p327 :023 > a1 = A.new
=> #<A:0x00000000e09b10>
1.9.3p327 :024 > a2 = AFancy.new.call
=> #<A:0x00000000e17210>
1.9.3p327 :025 > a3 = A.new
=> #<A:0x00000000e1bd38>
1.9.3p327 :026 > [a1, a2, a3].map(&:foo)
=> ["42", "ext", "42"]
Во-первых, есть что-то странное в ваших контекстах или, по крайней мере, в названии. Контекст не возвращает RolePlayers. Роли существуют только внутри контекста. Методы роли не являются и не должны быть доступны вне контекста. Тем не менее, стандартный способ работы с DCI в ruby - это внедрение метода. Этот подход не безупречен, но пока он наиболее близок к чистому DCI, который когда-либо был в Ruby. Есть экспериментальная библиотека с именем alias_dci, которая может помочь.
РЕДАКТИРОВАТЬ Теперь есть драгоценный камень, который делает возможным безинъекционный DCI в Ruby. Он основан на работе, проделанной в Marvin, первом языке, поддерживающем DCI без инъекций. Драгоценный камень называется Moby и может быть установлен с
gem install Moby
в настоящее время он все еще несколько экспериментален, но тест на дымность возможности применения примеров DCI из fullOO пройден