Help a ruby noob understand class inheritance
I'm trying to learn ruby by building a basic Campfire bot to screw around with at work. I've gotten pretty far (it works!) and learned a lot (it works!), but now I'm trying to make it a bit more complex by separating the actions to be performed out into their own classes, so that they can be easier to write / fix when broken. If you're interested in seeing all the (probably crappy) code, it's all up on GitHub. But for the sake of this question, I'll narrow the scope a bit.
Ideally, I would like to be able to create plugins easily, name them the same as the class name, and drop them into an "actions" directory in the root of the project, where they will be instantiated at runtime. I want the plugins themselves to be as simple as possible to write, so I want them all to inherit some basic methods and properties from an action
class.
Here is action.rb
as it currently exists:
module CampfireBot
class Action
@handlers = {}
def initialize(room)
@room = room
end
class << self
attr_reader :handlers
attr_reader :room
def hear(pattern, &action)
Action.handlers[pattern] = action
end
end
end
end
Where @room
is the room object, and @handlers
is a hash of patterns and blocks. I kind of don't understand why I have to do that class << self
call, but that's the only way I could get the child plugin classes to see that hear
method.
I then attempt to create a simple plugin like so (named Foo.rb
):
class Foo < CampfireBot::Action
hear /foo/i do
@room.speak "bar"
end
end
I then have my plugins instantiated inside bot.rb like so:
def load_handlers(room)
actions = Dir.entries("#{BOT_ROOT}/actions").delete_if {|action| /^\./.match(action)}
action_classes = []
# load the source
actions.each do |action|
load "#{BOT_ROOT}/actions/#{action}"
action_classes.push(action.chomp(".rb"))
end
# and instantiate
action_classes.each do |action_class|
Kernel.const_get(action_class).new(room)
end
@handlers = Action.handlers
end
The blocks are then called inside room.rb
when the pattern is matched by the following:
handlers.each do |pattern, acti开发者_如何学Goon|
if pattern.match(msg)
action.call($~)
end
end
If I do puts @room
inside the initialization of Action
, I see the room object printed out in the console. And if I do puts "foo"
inside Foo.rb
's hear
method, I see foo
printed out on the console (so, the pattern match is working). But, I can't read that @room
object from the parent class (it comes out as a nil object). So obviously I'm missing something about how this is supposed to be working.
Furthermore, if I do something to make the plugin a bit cleaner (for larger functions) and rewrite it like so:
class Foo < CampfireBot::Action
hear /foo/i do
say_bar
end
def say_bar
@room.speak "bar"
end
end
I get NoMethodError: undefined method 'say_bar' for Foo:Class
.
The definition of hear
can be pulled out of the class << self
block and changed to:
def self.hear(pattern, &action)
Action.handlers[pattern] = action
end
to yield the exact same result. That also immediately explains the problem. hear
Is a class method. say_bar
is an instance method. You can't call an instance method from a class method, because there simply isn't an instance of the class available.
To understand the class << self
bit, you'll have to do your own reading and experiments: I won't try to improve on what has already been said. I'll only say that within the class << self .. end
block, self
refers to the eigenclass or metaclass of the CampfireBot::Action
class. This is the instance of the Class
class that holds the definition of the CampfireBot::Action class.
精彩评论