Update: I've simplified my question; you can see the full history by checking out my editing revisions. Thanks to iain and bernardk for getting me this far.


I want to load carrierwave functionality into an instance of my User < ActiveRecord::Base model.

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

Executing:

(user = User.first).extend(HasAnAvatar).avatar

Results in:

NoMethodError: undefined method 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'

I suspect the problem is that mount_uploader in HasAnAvatar is not being invoked properly on the eigenclass for user, such that the uploaders hash isn't populated.

Any thoughts on how to get this working?


Here is an example Rails app for this issue: https://github.com/neezer/extend_with_avatar_example

有帮助吗?

解决方案 4

Ok, I think I found out what was causing my issues...

In https://github.com/jnicklas/carrierwave/blob/master/lib/carrierwave/mount.rb, in the definition of CarrierWave::Mount::Mounter, there are three references to record.class. This rightly references back to the main User class, which doesn't have the extended methods I loaded into the user's metaclass. So, I changed those to this: https://gist.github.com/4465172, and it seemed to work.

Also seems to continue to work if used normally like in the CarrierWave docs, so that's good too. Will continue testing it, though.

其他提示

Here are two ways you can can include a module into an instance (which is to basically extend the instance's eigenclass). I don't think this is the best answer to your problem though, even if it may answer the question (in part).

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>

From your edit:

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>

From what I know of Ruby classes, once I include a module into a class, ... but not retroactively alter any existing instances of User.

On the contrary, an include/extend immediately affects all existing instances, because it is a question of pointer between the class and its superclass. See How does Inheritance work in Ruby? and also the links inside.

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

Execution (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

So included methods immediately become available to all existing instances.
Why does user.avatar answer nil ? Because instance variables belong to ... single instances. See Why are symbols in Ruby not thought of as a type of variable? In this case, the old user was not assigned an avatar.

But why do you get user2.avatar.class #=> AvatarUploader. I suppose that, behind the scene, base.mount_uploader :avatar, AvatarUploader does something such that :avatar is not an accessor of a corresponding instance variable, or defines an initialize method which starts to set this variable into new instances.


Here is a solution for the second example :

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  

Execution :

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


About jelly.class.pacified_with(:polar_bear) : the mistake was to call class, which answers the class JellyFish; what you want is the singleton class of the instance object jelly. Once a method is defined in the singleton class of an object, you can send it directly to the object. The same applies to classes, which are instances of Class. Once a method is defined in the singleton class of a class, you can send it directly to the class. We call them class methods, they are actually instance methods of the singleton class of the class. Ouf !


Last OR : as explained, if you extend the class, it is valid for all existing instances. Thus you have to extend individual instances :

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?

Execution :

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)
  1. First of all, the way with instance extend calls is working (See end of the post).

    1. Then you should take into account some opinions that this is bad for performance
    2. And at last, according to sources, it should work as expected. So I suggest you to wear your debug hat and try to see what exactly happens on user.extend part. For example, see if mount_uploader uses Carrierwave::Mount#mount_uploader method, as there are defined '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"]

Firstly there's something odd about your contexts or at least the naming. Context do not return RolePlayers. Roles exist only inside a context. The role methods are not and should not be accessible outside the context. That said the standard way of dealing with DCI in ruby is method injection. This approach is not flawless but is so far the closest to pure DCI anyone have come in Ruby. There's an experimental library called alias_dci that might help.

EDIT There now is a gem that makes injectionless DCI possible in Ruby. It's based on the work gone into Marvin, the first language to support injectionless DCI. The gem is called Moby and can be installed with

gem install Moby

it's currently still somewhat experimental but the smoke test of being able to implement the DCI examples from fullOO has been passed

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top