扩展 ruby 特征类以加载 CarrierWave
Posted
技术标签:
【中文标题】扩展 ruby 特征类以加载 CarrierWave【英文标题】:Extending a ruby eigenclass to load CarrierWave 【发布时间】:2012-12-19 16:06:02 【问题描述】:更新:我已经简化了我的问题;您可以通过查看my editing revisions 查看完整的历史记录。感谢 iain 和 bernardk 让我走到这一步。
我想将载波功能加载到我的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
uploader'
我怀疑问题是HasAnAvatar
中的mount_uploader
没有在user
的特征类上正确调用,因此the uploaders
hash isn't populated。
关于如何使它工作的任何想法?
以下是针对此问题的 Rails 应用示例:https://github.com/neezer/extend_with_avatar_example
【问题讨论】:
除了我的回答,你还需要了解方法搜索机制。请阅读***.com/questions/14088513/… 我无法重现该问题。你有'uploaders/avatar_uploader'
的链接吗?还有mount.rb
.
@BernardK mount.rb
是 carrierwave
的一部分(在 gem 中)。重新审视我的 avatar_uploader
课程,看看它是否在做某事,而股票 carrierwave
上传者不是...
@BernardK 这是一个重现此问题的示例 Rails 应用程序:github.com/neezer/extend_with_avatar_example(对自述文件感到抱歉——Github 拒绝将其作为降价文件读取,并一直坚持其和 rdoc ......很奇怪)
【参考方案1】:
首先,您的上下文或至少命名有些奇怪。上下文不返回 RolePlayers。角色仅存在于上下文中。角色方法不能也不应该在上下文之外访问。也就是说,在 ruby 中处理 DCI 的标准方法是方法注入。这种方法并非完美无缺,但迄今为止最接近 Ruby 中的纯 DCI。有一个名为 alias_dci 的实验性库可能会有所帮助。
编辑 现在有一个 gem 可以在 Ruby 中实现无注入 DCI。它基于 Marvin 的工作,这是第一种支持无注入 DCI 的语言。 gem 名为Moby,可以使用
gem install Moby
目前仍处于试验阶段,但能够实现来自fullOO 的 DCI 示例的冒烟测试已经通过
【讨论】:
这里仍在学习 DCI,所以就实际模式而言,我可能还没有完全正确。我通常会返回 RolePlayers,以便我可以使用 Railsrespond_with
它们来保持我的控制器超级干燥。我也会看看你建议的那个库。
@neezer 它可能会起作用并按照您的预期进行,但它在 DCI 的要点之一上有所松动。每个操作的代码都位于一个位置,而不是分布在多个类中
尝试实施 Jim Gay 的 Clean Ruby 建议,Loc。 1505(Kindle、MOBI 版),0.5 版
我没有读过 Jim 的书,但只和他辩论过,当谈到 DCI 时,他通常很清楚。那么您是否正在更改示例以适应另一个难题?
我们也可能在互相交谈:我不打算使用 RolePlayer 作为其他函数的输入:在这种情况下,它是上下文的输出,它被直接馈送到Rails 响应器并作为最终输出返回给客户端。我正在检查我发布的问题中的输出以确定输出是否正确,因此我在问题中的命名可能与我实际打算完成的意图不符。【参考方案2】:
好的,我想我找到了导致我的问题的原因......
在https://github.com/jnicklas/carrierwave/blob/master/lib/carrierwave/mount.rb中,CarrierWave::Mount::Mounter
的定义中,有三个对record.class
的引用。这正确地引用了主 User
类,它没有我加载到用户元类中的扩展方法。所以,我将它们更改为:https://gist.github.com/4465172,它似乎工作了。
如果像在 CarrierWave 文档中一样正常使用,似乎也可以继续工作,所以这也很好。不过会继续测试它。
【讨论】:
我能够重现该错误。这个 mount.rb 是一个精彩的 Ruby 元编程!一个 class_eval-ed 方法调用 super,那里没有超类,但是一个模块很快就会被包含但还不存在,因为它将完全是 class_eval-ed !干得好!【参考方案3】:根据我对 Ruby 类的了解,一旦我将一个模块包含到一个 类,...但不追溯更改任何现有实例 用户。
相反,包含/扩展会立即影响所有现有实例,因为它是类与其超类之间的指针问题。请参阅How does Inheritance work in 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
回答 nil ?因为实例变量属于......单个实例。见Why are symbols in Ruby not thought of as a type of variable?
在这种情况下,没有为旧用户分配头像。
但是为什么你会得到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
类;你想要的是实例对象果冻的单例类。一旦在对象的单例类中定义了方法,就可以将其直接发送给对象。这同样适用于作为 Class 的实例的类。一旦在类的单例类中定义了方法,就可以直接将其发送给该类。我们称它们为类方法,它们实际上是类的单例类的实例方法。哎哟!
最后一个 OR :如解释的那样,如果您扩展类,它对所有现有实例都有效。因此,您必须扩展单个实例:
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)
【讨论】:
感谢详细解答;我想我从我的第二个例子中理解了你关于单例类的观点,但我不确定我是否遵循你关于我的第一个例子(使用 Carrierwave)的观点。具体来说,不确定执行块中的“设置后”是什么意思。你能澄清一下吗? @neezer 只是为了强调实例变量@avatar
属于这个单实例用户,可以随时单独设置。我不明白的是所有用户的user.avatar.class #=> AvatarUploader
。如果avatar
有一个attribute_reader,它应该回答nil 或任何分配给它的东西。 p user.avatar.class
"after set" 显示的是,如果 avatar
是一个属性读取器,并且当且仅当我自己设置该值,则返回该值是AvatarUploader
。 (续...)
@neezer ...当您说我希望 user.avatar.class #=> AvatarUploader
和 user2.avatar.class #=> String or Nil
并且似乎使其依赖于 User.send :include, HasAnAvatar
时,我说不,它仅取决于对每个单独用户的分配实例。 HTH :)
CarrierWave(我认为)overlays logic on top of the attribute_reader, then super's down into the original behavior。我是making progress extending the eigenclass of the user,但还没有到。还有其他想法吗?【参考方案4】:
这里有两种方法可以将模块包含到实例中(基本上是扩展实例的特征类)。我不认为这是对您的问题的最佳答案,即使它可以回答问题(部分)。
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>
【讨论】:
在这里完全理解您的示例,扩展 eigenclass 似乎让我朝着我所追求的方向走得更远,尽管它仍然不能完全与 CarrierWave 一起使用。 Here's what I have currently;有什么想法吗? @neezerif self.class == Class
应该是 if host.class == Class
,因为这里的 self 始终是模块 HasAnAvatar
。要跟踪 self 的值,请插入 puts #self
语句。在我的Pacifiable
版本中,self 的值在def host.pacified_with
之后发生了变化。这在pragprog.com/book/ppmetr/metaprogramming-ruby 中有很好的解释。
@bernardk 你对host.class
的事情是正确的;错过了。会更新。但是,仍然会出现同样的错误。【参考方案5】:
首先,实例extend
调用的方式是有效的(见帖子末尾)。
-
那你应该考虑some opinions that this is bad for performance
最后,根据sources,它应该可以按预期工作。所以我建议你戴上你的调试帽,试着看看
user.extend
部分到底发生了什么。例如,查看 mount_uploader
是否使用 Carrierwave::Mount#mount_uploader
方法,因为定义了“上传者”everrides。
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"]
【讨论】:
嗯,您的示例仅使用实例方法,而不是像 Carrierwave 那样在实例化时定义新方法的类方法。也许我必须想出一个更具说明性的例子...... 它没有定义实例化的新方法。据我所知,它只是创建带有覆盖的新模块并包含它。所有这些都发生在mount_*
调用上,即在应用程序启动时的课堂评估时间。在您的情况下,它不会是一次,但似乎仍然没有区别。
"at class eval time once on app start":但只有当我的ActiveRecord::Base
模型包含对mount_uploader
的调用时,我才试图避免这种情况(这就是我的问题)。我希望能够在内部传递用户对象,而不总是伴随着 CarrierWave 头像方法,所以我试图弄清楚如何仅在需要它们时才包含它们。
在上面添加了另一个示例,这(希望)更有意义。你能看一下吗?以上是关于扩展 ruby 特征类以加载 CarrierWave的主要内容,如果未能解决你的问题,请参考以下文章
使用自定义扩展将 Ruby 应用程序部署到 Elastic Beanstalk,无法加载编译文件