A problem I'm having with modules
I'm trying to define a static variable and methods in a module that will be extended/used by numerous classes. The following example demonstrates:
module Ammunition
def self.included(base)
base.class_eval("@@ammo = [bullets]")
end
def unload
p @@ammo #<-- doesn't work
end
end
class Tank
include Ammunition
@@a += [shells]
end
class Airplane
include Ammunition
@@a += [missiles, photon_torpedoes]
end
Tank.new.unload
Airplane.new.unload
This doesn't work because ammunition doesn't know how to evaluate @@ammo in the context of the class for some reason (I original t开发者_开发技巧hought the module would behave just like an include file). I would have to copy 'unload' to each class, which I'm doing right now, but I want to DRY it up b/c I have many other methods to add to the module.
Suggestions? The reasonable solution would be to evaluate 'unload' in the context of the class and not the module (but how to do this in Ruby?)
Thanks!
class variables can work strangely, and this use shows that off. What is the scope of @@ammo
? Ammunition
or does Tank
have its own copy of it? It turns out that @@ammo
is scoped by the module, and the classes that include it can simply access it.
module Ammunition
def self.included(base)
base.class_eval do
puts "@@ammo was: #{defined?(@@ammo) ? @@ammo.join(',') : 'nil'}"
@@ammo = ['bullets']
puts "@@ammo is now: #{@@ammo}"
puts '---'
end
end
def unload
@@ammo
end
end
class Tank
include Ammunition
@@ammo += ['shells']
end
class Airplane
include Ammunition
@@ammo += ['missiles', 'photon_torpedoes']
end
puts "Tank unloaded: #{Tank.new.unload.join(', ')}"
puts "Airplane unloaded: #{Airplane.new.unload.join(', ')}"
This produces:
@@ammo was: nil
@@ammo is now: bullets
---
@@ammo was: bullets,shells
@@ammo is now: bullets
---
Tank unloaded: bullets, missiles, photon_torpedoes
Airplane unloaded: bullets, missiles, photon_torpedoes
When Tank
includes the module, it sets @@ammo
from nil to an array with bullets in it. When Airplane
includes the module, it overwrites the ammo value we just set.
Here is what you want to do
module Ammunition
def self.included(base)
base.class_eval do
include Ammunition::InstanceMethods
extend Ammunition::ClassMethods
@ammo = ['bullets']
end
end
module ClassMethods
def ammo
@ammo
end
end
module InstanceMethods
def unload
self.class.ammo.join(',')
end
end
end
class Tank
include Ammunition
@ammo += ['shells']
end
class Airplane
include Ammunition
@ammo += ['missiles', 'photon_torpedoes']
end
puts "Tank unloaded: #{Tank.new.unload}"
puts "Airplane unloaded: #{Airplane.new.unload}"
Classes can have instance variables, and their scope is easier to understand. And separating your module into instance and class methods allow you to provide functionality to both. This snippet generates the following output
Tank unloaded: bullets,shells
Airplane unloaded: bullets,missiles,photon_torpedoes
Well, first of all... it's a really good idea to explain how @@
variables work exactly.
@@
variables are class variables that can be accessed on the instance context, say for example:
class Klass
def my_klass_variable=(str)
# self here points to an instance of Klass
@@my_klass_variable = str
end
def my_klass_variable
@@my_klass_variable
end
end
Klass.new.my_klass_variable = "Say whaat?"
# Note this is a different instance
Klass.new.my_klass_variable # => "Say whaat?"
However this type of variables will incur also in the following result:
class OtherKlass < Klass; end
Klass.new.my_klass_variable = "Howdy"
# Note this is a different instance, and from the child class
OtherKlass.new.my_klass_variable # => "Howdy"
Crazy behavior indeed. Another way to create Class variables, is defining instance variables on a method that starts with self.
. For example:
class Klass
def self.my_class_method
@class_var = "This is a class var"
end
end
Why a @
for class variables as well? Remember that Klass
in this is an instance of the Class
class, this will have its own instance variables, that at the end will be class variables for instances of Klass
.
Klass.class # => Class
Klass.instance_of?(Class) # => true
k = Klass.new
k.class # => Klass
k.instance_of?(Klass) # => true
This is more safe for class variables (as they will have one copy of the variable, and not a shared one with child classes as well), and will behave as you are expecting to behave when using your example:
module Ammunition
def self.included(base)
base.class_eval do
@ammo = [bullets] # where bullets come from any way?
end
end
def self.unload
p @ammo
end
end
class Tank
include Ammunition # Probably you meant that instead of Packagable
@ammo += [shells] # I think you meant @ammo instead of @a
end
class Airplane
include Ammunition # Probably you meant that instead of Packagable
@ammo += [missiles, photon_torpedoes] # I think you meant @ammo instead of @a
end
This code as pointed by others won't work (given there is no shells, missiles nor photo_torpedoes), but I think you can figure it out how to make it work by yourself.
A few issues:
(1) The module name ammunition
must start with a capital -- Ammunition
(2) you're including Packagable
into your classes but i assume you mean Ammunition
?
(3) all of your variables - missiles
, photon
and photon_torpedos
are undefined, so your code does not actually run.
I suggest you first fix this code :) But as an aside, class variables @@myvar
are considered a no-no among most Rubyists.
精彩评论