Saturday, October 24, 2009

Recipe 8.2. Managing Class Data










Recipe 8.2. Managing Class Data







Problem


Instead of storing a bit of data along with every instance of a class, you want to store a bit of data along with the class itself.




Solution


Instance
variables are prefixed by a single at sign; class
variables are prefixed by two at signs. This class contains both an instance variable and a class variable:



class Warning
@@translations = { :en => 'Wet Floor',
:es => 'Piso Mojado' }

def initialize(language=:en)
@language = language
end

def warn
@@translations[@language]
end
end

Warning.new.warn # => "Wet Floor"
Warning.new(:es).warn # => "Piso Mojado"





Discussion



Class variables store information that's applicable to the class itself, or applicable to every instance of the class. They're often used to control, prevent, or react to the
instantiation of the class. A class variable in Ruby acts like a static variable in Java.


Here's an example that uses a class constant and a class variable to control when and how a class can be instantiated:



class Fate
NAMES = ['Klotho', 'Atropos', 'Lachesis'].freeze
@@number_instantiated = 0

def initialize
if @@number_instantiated >= NAMES.size
raise ArgumentError, 'Sorry, there are only three Fates.'
end
@name = NAMES[@@number_instantiated]
@@number_instantiated += 1
puts "I give you… #{@name}!"
end
end

Fate.new
# I give you… Klotho!
# => #<Fate:0xb7d2c348 @name="Klotho">

Fate.new
# I give you… Atropos!
# => #<Fate:0xb7d28400 @name="Atropos">

Fate.new
# I give you… Lachesis!
# => #<Fate:0xb7d22168 @name="Lachesis">

Fate.new
# ArgumentError: Sorry, there are only three Fates.



It's not considered good form to write
setter or
getter methods for
class variables. You won't usually need to expose any class-wide information apart from helpful constants, and those you can expose with class constants such as NAMES above.


If you do want to write setter or getter methods for
class variables, you can use the following class-level equivalents of Module#attr_reader and Module#attr_writer. They use metaprogramming to define new accessor methods: [1]

[1] In Ruby 1.9, Object#send can't be used to call private methods. You'll need to replace the calls to send with calls to Object#funcall.



class Module
def class_attr_reader(*symbols)
symbols.each do |symbol|
self.class.send(:define_method, symbol) do
class_variable_get("@@#{symbol}")
end
end
end

def class_attr_writer(*symbols)
symbols.each do |symbol|
self.class.send(:define_method, "#{symbol}=") do |value|
class_variable_set("@@#{symbol}", value)
end
end
end

def class_attr_accessor(*symbols)
class_attr_reader(*symbols)
class_attr_writer(*symbols)
end
end



Here is Module#class_attr_reader being used to give the Fate class an accessor for its class variable:



Fate.number_instantiated
# NoMethodError: undefined method 'number_instantiated' for Fate:Class

class Fate
class_attr_reader :number_instantiated
end
Fate.number_instantiated # => 3



You can have both a class variable foo and an instance variable foo, but this will only end up confusing you. For instance, the accessor method foo must retrieve one or the other. If you call attr_accessor :foo and then class_attr_accessor :foo, the class version will silently overwrite the instance version.


As with instance variables, you can bypass encapsulation and use class variables directly with class_variable_get and class_variable_set. Also as with instance variables, you should only do this from inside the class, usually within a define_method call.




See Also


  • If you want to create a singleton, don't mess around with class variables; instead, use the singleton library from Ruby's standard library

  • Recipe 8.18, "Implementing Class and Singleton Methods"

  • Recipe 10.10, "Avoiding Boilerplate Code with Metaprogramming"













No comments:

Post a Comment