Search code examples
rubyclass-variables

Is there a better way of doing class_eval() to extract class variables, in Ruby?


I personally don't have anything against this, apart from the fact that's is long, but what really bothers me is the word eval.

I do a lot of stuff in JavaScript and I run from anything resembling eval like it's the devil, I also don't fancy the fact that the parameter is a string (again, probably because it's eval).

I know I could write my own method to fix the method-name-length problem, my 'method name issue' and the parameter-being-a-string thingy, but what I really want to know is: Is there a better, shorter, fancier, yet native, way of doing class_eval to extract class variables?

Side note: I know about the existence of class_variable_get() and class_variables(), but they don't really look appealing to me; horribly long, aren't they?

EDIT: Updated the question to be more specific.

Thanks!


Solution

  • Use class_variable_get, but only if you must

    class_variable_get is the better way, other than the fact that it is not "appealing" to you. If you are reaching inside a class and breaking encapsulation, perhaps it is appropriate to have this extra barrier to indicate that you're doing something wrong.

    Create accessor methods for the variables you want to access

    If these are your classes, and accessing the variables doesn't break encapsulation, then you should create class accessor methods for them to make it easier and prettier:

    class Foo
      def self.bar
        @@bar
      end
    end
    p Foo.bar
    

    If this is your class, however, are you sure that you need class variables? If you don't understand the implications (see below), you may actually be wanting instance variables of the class itself:

    class Foo
      class << self
        attr_accessor :bar
      end
    end
    
    Foo.bar = 42
    p Foo.bar
    

    The behavior of class variables

    Class variables appear to newcomers like the right way to store information at a class level, mostly because of the name. They are also convenient because you can use the same syntax to read and write them whether you are in a method of the class or an instance method. However, class variables are shared between a class and all its subclasses.

    For example, consider the following code:

    class Rectangle
      def self.instances
        @@instances ||= []
      end
      def initialize
        (@@instances ||= []) << self
      end
    end
    
    class Square < Rectangle
      def initialize
        super
      end
    end
    
    2.times{ Rectangle.new }
    p Rectangle.instances
    #=> [#<Rectangle:0x25c7808>, #<Rectangle:0x25c77d8>]
    
    Square.new
    p Square.instances
    #=> [#<Rectangle:0x25c7808>, #<Rectangle:0x25c77d8>, #<Square:0x25c76d0>]
    

    Ack! Rectangles are not squares! Here's a better way to do the same thing:

    class Rectangle
      def self.instances
        @instances ||= []
      end
      def initialize
        self.class.instances << self
      end
    end
    
    class Square < Rectangle
      def initialize
        super
      end
    end
    
    2.times{ Rectangle.new }
    p Rectangle.instances
    #=> [#<Rectangle:0x25c7808>, #<Rectangle:0x25c77d8>]
    
    2.times{ Square.new }
    p Square.instances
    #=> [#<Square:0x25c76d0>, #<Square:0x25c76b8>]
    

    By creating an instance variable and accesor methods on the class itself—which happens to be an instance of the Class class, similar to MyClass = Class.new—all instances of the class (and outsiders) have a common, clean location to read/write information that is not shared between other classes.

    Note that explicitly tracking every instance created will prevent garbage collection on 'unused' instances. Use code like the above carefully.

    Using class_eval in a cleaner manner

    Finally, if you're going to use class_eval, note that it also has a block form that doesn't have to parse and lex the string to evaluate it:

    Foo.class_eval('@@bar') # ugh
    Foo.class_eval{ @@bar } # yum