Accessing a class's containing namespace from within a module
I'm working on a module that, among other things, will add some generic 'finder' type functionality to the class you mix it into. The problem: for reasons of convenience and aesthetics, I want to include some functionality outside the class, in the same scope as the class itself.
For example:
class User
include MyMagicMixin
end
# Should automagically enable:
User.name('Bob') # Returns first user named Bob
Users.name('Bob') # Returns ALL users named Bob
User(5) # Returns the user with an ID of 5
Users # Returns all users
I can do the functionality within these methods, no problem. And case 1 (User.name('Bob')
) is easy. Cases 2–4, however, require being able to create new classes and methods outside User
. The Module开发者_如何学Python.included
method gives me access to the class, but not to its containing scope. There is no simple "parent" type method that I can see on Class nor Module. (For namespace, I mean, not superclass nor nested modules.)
The best way I can think to do this is with some string parsing on the class's #name
to break out its namespace, and then turn the string back into a constant. But that seems clumsy, and given that this is Ruby, I feel like there should be a more elegant way.
Does anyone have ideas? Or am I just being too clever for my own good?
I'd lean toward being too clever.
Even if there was an elegant solution, it seems rather odd to be including a module inside a class which creates classes outside the class.
This is a problem that comes up sometimes on the mailinglists. It's also a problem that comes up in Rails. The solution is, as you already suspected, basically Regexp munging.
However, there is a more fundamental problem: in Ruby, classes do not have a name! A class is just an object like any other. You can assign it to an instance variable, to a local variable, to a global variable, to a constant or even not assign it to anything at all. The Module#name
method is basically just a convenience method that works like this: it looks through the list of defined constants until it finds one that points to the receiver. If it finds one, it returns the first one it can find, otherwise it returns nil
.
So, there's two failure modes here:
a = Class.new
a.name # => nil
B = a
B.name # => "B"
A = B
A.name # => "B"
- a class might not have a name at all
- a class might have more than one name, but
Module#name
will only return the first one it finds
Now, if someone tries to call As
to get a list of A
s, they will be pretty surprised to find that that method doesn't exist, but that they can call Bs
instead to get the same result.
This does actually happen in reality. In MacRuby, for example String.name
returns NSMutableString
, Hash.name
returns NSMutableDictionary
and Object.name
returns NSObject
. The reason for this is that MacRuby integrates the Ruby runtime and the Objective-C runtime into one, and since the semantics of an Objective-C mutable string are identical to a Ruby string, the entire implementation of Ruby's string class is essentially a single line: String = NSMutableString
. And since MacRuby sits on top of Objective-C, that means that Objective-C starts first, which means that NSMutableString
gets inserted into the symbol table first, which means it gets found first by Module#name
.
In your example, User
is just a constant that points to a Class
object. You can easily create another constant pointer when MyMagicMixin
is included:
module MyMagicMixin
class <<self
def self.included(klass)
base.extend MyMagicMixin::ClassMethods
create_pluralized_alias(klass)
end
private
def create_pluralized_alias(klass)
fq_name = klass.to_s
class_name = fq_name.demodulize
including_module = fq_name.sub(Regexp.new("::#{class_name}$", ''))
including_module = including_module.blank? ? Object : including_module.constantize
including_module.const_set class_name.pluralize, klass
end
end
module ClassMethods
# cass methods here
end
end
Of course this doesn't answer whether you should do such a thing.
精彩评论