We begin our coverage of classes with an extended tutorial
that develops a class named Point
to
represent a geometric point with X and Y coordinates. The subsections
that follow demonstrate how to:
Define a new class
Create instances of that class
Write an initializer method for the class
Add attribute accessor methods to the class
Define operators for the class
Define an iterator method and make the class Enumerable
Override important Object
methods such as to_s
, ==
, hash
, and <=>
Define class methods, class variables, class instance variables, and constants
Classes are created in Ruby with the class
keyword:
class Point end
Like most Ruby constructs, a class definition is delimited with
an end
. In addition to defining a
new class, the class
keyword
creates a new constant to refer to the class. The class name and the
constant name are the same, so all class names must begin with a
capital letter.
Within the body of a class
,
but outside of any instance methods defined by the class, the self
keyword refers to the class being
defined.
Like most statements in Ruby, class
is an expression. The value of a
class
expression is the value of
the last expression within the class
body. Typically, the last expression
within a class is a def
statement
that defines a method. The value of a def
statement is always nil
.
Even though we haven’t put anything in our Point
class yet, we can still instantiate
it:
p = Point.new
The constant Point
holds a
class object that represents our new class. All class objects have a
method named new
that creates a new
instance.
We can’t do anything very interesting with the newly created
Point
object we’ve stored in the
local variable p
, because we
haven’t yet defined any methods for the class. We can, however, ask
the new object what kind of object it is:
p.class # => Point p.is_a? Point # => true
When we create new Point
objects, we want to initialize them with two numbers that represent
their X and Y coordinates. In many object-oriented languages, this is
done with a “constructor.” In Ruby, it is done with an
initialize
method:
class Point def initialize(x,y) @x, @y = x, y end end
This is only three new lines of code, but there are a couple of
important things to point out here. We explained the def
keyword in detail in Chapter 6. But that chapter focused on defining global
functions that could be used from anywhere. When def
is used like this with an unqualified
method name inside of a class
definition, it defines an instance method for the
class. An instance method is a method that is invoked on an instance
of the class. When an instance method is called, the value of self
is an instance of the class in which
the method is defined.
The next point to understand is that the initialize
method has a special purpose in
Ruby. The new
method of the class
object creates a new instance object, and then it automatically invokes the initialize
method on that instance. Whatever
arguments you passed to new
are
passed on to initialize
. Because
our initialize
method expects two
arguments, we must now supply two values when we invoke Point.new
:
p = Point.new(0,0)
In addition to being automatically invoked by Point.new
, the initialize
method is automatically made
private. An object can call initialize
on itself, but you cannot
explicitly call initialize
on
p
to reinitialize its state.
Now, let’s look at the body of the initialize
method. It takes the two values
we’ve passed it, stored in local variables x
and y
,
and assigns them to instance variables @x
and @y
. Instance variables always begin with
@
, and they always “belong to”
whatever object self
refers to.
Each instance of our Point
class
has its own copy of these two variables, which hold its own X and Y
coordinates.
Finally, a caution for programmers who are used to Java and related languages. In statically typed languages, you must declare your variables, including instance variables. You know that Ruby variables don’t need to be declared, but you might still feel that you have to write something like this:
# Incorrect code! class Point @x = 0 # Create instance variable @x and assign a default. WRONG! @y = 0 # Create instance variable @y and assign a default. WRONG! def initialize(x,y) @x, @y = x, y # Now initialize previously created @x and @y. end end
This code does not do at all what a Java programmer expects.
Instance variables are always resolved in the context of self
. When the initialize
method is invoked, self
holds an instance of the Point
class. But the code outside of that
method is executed as part of the definition of the Point
class. When those first two
assignments are executed, self
refers to the Point
class itself,
not to an instance of the class. The @x
and @y
variables inside the initialize
method are completely different from those outside it.
Just about any class you define should have a to_s
instance method to return a string representation of the
object. This ability proves invaluable when debugging. Here’s how we
might do this for Point
:
class Point def initialize(x,y) @x, @y = x, y end def to_s # Return a String that represents this point "(#@x,#@y)" # Just interpolate the instance variables into a string end end
With this new method defined, we can create points and print them out:
p = Point.new(1,2) # Create a new Point object puts p # Displays "(1,2)"
Our Point
class
uses two instance variables. As we’ve noted, however, the value
of these variables are only accessible to other instance methods. If
we want users of the Point
class to
be able to use the X and Y coordinates of a point, we’ve got to
provide accessor methods that return the value of the
variables:
class Point def initialize(x,y) @x, @y = x, y end def x # The accessor (or getter) method for @x @x end def y # The accessor method for @y @y end end
With these methods defined, we can write code like this:
p = Point.new(1,2) q = Point.new(p.x*2, p.y*3)
The expressions p.x
and
p.y
may look like variable
references, but they are, in fact, method invocations without
parentheses.
If we wanted our Point
class
to be mutable (which is probably not a good idea), we would also add
setter methods to set the value of the instance variables:
class MutablePoint def initialize(x,y); @x, @y = x, y; end def x; @x; end # The getter method for @x def y; @y; end # The getter method for @y def x=(value) # The setter method for @x @x = value end def y=(value) # The setter method for @y @y = value end end
Recall that assignment expressions can be used to invoke setter methods like these. So with these methods defined, we can write:
p = Point.new(1,1) p.x = 0 p.y = 0
This combination of instance variable with trivial getter and
setter methods is so common that Ruby provides a way to automate it.
The attr_reader
and attr_accessor
methods are defined by the Module
class. All
classes are modules, (the Class
class is a subclass of Module
) so
you can invoke these methods inside any class definition. Both methods
take any number of symbols naming attributes. attr_reader
creates trivial getter methods
for the instance variables with the same name. attr_accessor
creates getter and setter
methods. (The infrequently used attr_writer
creates setter methods only.)
Thus, if we were defining a mutable Point
class, we could write:
class Point attr_accessor :x, :y # Define accessor methods for our instance variables end
And if we were defining an immutable version of the class, we’d write:
class Point attr_reader :x, :y # Define reader methods for our instance variables end
Each of these methods can accept an attribute name or names as a string rather than as a symbol. The accepted style is to use symbols, but we can also write code like this:
attr_reader "x", "y"
attr
is a similar method with
a shorter name but with behavior that differs in Ruby 1.8 and Ruby
1.9. In 1.8, attr
can define only a
single attribute at a time. With a single symbol argument, it defines
a getter method. If the symbol is followed by the value true
, then it defines a setter method as
well:
attr :x # Define a trivial getter method x for @x attr :y, true # Define getter and setter methods for @y
In Ruby 1.9, attr
can be used
as it is in 1.8, or it can be used as a synonym for attr_reader
.
The attr
, attr_reader
, and attr_accessor
methods create instance
methods for us. This is an example of
metaprogramming, and the ability to do it is a powerful feature of Ruby.
There are more examples of metaprogramming in Chapter 8.
Note that attr
and its related
methods are invoked within a class
definition but outside of any method definitions. They are only
executed once, when the class is being defined. There are no
efficiency concerns here: the getter and setter methods they create
are just as fast as handcoded ones. Remember that these methods are
only able to create trivial getters and setters that map directly to
the value of an instance variable with the same name. If you need more
complicated accessors, such as setters that set a differently named
variable, or getters that return a value computed from two different
variables, then you’ll have to define those yourself.
We’d like the +
operator
to perform vector addition of two Point
objects, the *
operator to multiply a Point
by a scalar, and the unary –
operator to do the equivalent of
multiplying by –1
. Method-based
operators such as +
are simply
methods with punctuation for names. Because there are unary and binary
forms of the –
operator, Ruby uses
the method name –@
for unary minus.
Here is a version of the Point
class with mathematical operators defined:
class Point attr_reader :x, :y # Define accessor methods for our instance variables def initialize(x,y) @x,@y = x, y end def +(other) # Define + to do vector addition Point.new(@x + other.x, @y + other.y) end def -@ # Define unary minus to negate both coordinates Point.new(-@x, -@y) end def *(scalar) # Define * to perform scalar multiplication Point.new(@x*scalar, @y*scalar) end end
Take a look at the body of the +
method. It is able to use the @x
instance variable of self
—the object that the method is invoked
on. But it cannot access @x
in the
other Point
object. Ruby simply
does not have a syntax for this; all instance variable references
implicitly use self
. Our +
method, therefore, is dependent on the
x
and y
getter methods. (We’ll see later that it
is possible to restrict the visibility of methods so that objects of
the same class can use each other’s methods, but code outside the
class cannot use them.)
Note that our *
method
expects a numeric operand, not a Point
. If p
is point, then we can write p*2
. As our class is written, however, we
cannot write 2*p
. That second
expression invokes the *
method of the Integer
class, which doesn’t know how to
work with Point
objects. Because
the Integer
class doesn’t know how
to multiply by a Point
, it asks the
point for help by calling its coerce
method. (See Arithmetic operator type coercions for more details.) If we want the expression
2*p
to return the same result as
p*2
, we can define a coerce
method:
# If we try passing a Point to the * method of an Integer, it will call # this method on the Point and then will try to multiply the elements of # the array. Instead of doing type conversion, we switch the order of # the operands, so that we invoke the * method defined above. def coerce(other) [self, other] end
Ruby uses square brackets for array and hash access, and allows any
class to define a []
method and use
these brackets itself. Let’s define a []
method for our class to allow Point
objects to be treated as read-only
arrays of length 2
, or as read-only
hashes with keys :x
and :y
:
# Define [] method to allow a Point to look like an array or # a hash with keys :x and :y def [](index) case index when 0, -2: @x # Index 0 (or -2) is the X coordinate when 1, -1: @y # Index 1 (or -1) is the Y coordinate when :x, "x": @x # Hash keys as symbol or string for X when :y, "y": @y # Hash keys as symbol or string for Y else nil # Arrays and hashes just return nil on bad indexes end end
If a Point
object can behave
like an array with two elements, then perhaps we ought to be able to
iterate through those elements as we can with a true array. Here is a
definition of the each
iterator for
our Point
class. Because a Point
always has exactly two elements, our
iterator doesn’t have to loop; it can simply call yield
twice:
# This iterator passes the X coordinate to the associated block, and then # passes the Y coordinate, and then returns. It allows us to enumerate # a point as if it were an array with two elements. This each method is # required by the Enumerable module. def each yield @x yield @y end
With this iterator defined, we can write code like this:
p = Point.new(1,2) p.each {|x| print x } # Prints "12"
More importantly, defining the each
iterator allows us to mix in the
methods of the Enumerable
module,
all of which are defined in terms of each
. Our class gains over 20 iterators by
adding a single line:
include Enumerable
If we do this, then we can write interesting code like this:
# Is the point P at the origin? p.all? {|x| x == 0 } # True if the block is true for all elements
As our class is currently defined, two distinct Point
instances are never equal to each
other, even if their X and Y coordinates are the same. To remedy this,
we must provide an implementation of the ==
operator. (You
may want to reread Object Equality in Chapter 3 to refresh your memory about Ruby’s various
notions of equality.)
Here is an ==
method for
Point
:
def ==(o) # Is self == o? if o.is_a? Point # If o is a Point object @x==o.x && @y==o.y # then compare the fields. else # If o is not a Point false # then, by definition, self != o. end end
Recall from Object Equality that Ruby objects also
define an eql?
method for testing
equality. By default, the eql?
method, like the ==
operator, tests
object identity rather than equality of object content. Often, we want
eql?
to work just like the ==
operator, and we can accomplish this with
an alias:
class Point alias eql? == end
On the other hand, there are two reasons we might want eql?
to be different from ==
. First, some classes define eql?
to perform a stricter comparison than
==
. In Numeric
and its subclasses, for example,
==
allows type conversion and
eql?
does not. If we believe that
the users of our Point
class might
want to be able to compare instances in two different ways, then we
might follow this example. Because points are just two numbers, it
would make sense to follow the example set by Numeric
here. Our eql?
method would look much like the
==
method, but it would use
eql?
to compare point coordinates
instead of ==
:
def eql?(o) if o.instance_of? Point @x.eql?(o.x) && @y.eql?(o.y) else false end end
As an aside, note that this is the right approach for any
classes that implement collections (sets, lists, trees) of arbitrary
objects. The ==
operator should
compare the members of the collection using their ==
operators, and the eql?
method should compare the members using their eql?
methods.
The second reason to implement an eql?
method that is different from the ==
operator is if you want instances of your
class to behave specially when used as a hash key. The Hash
class uses eql?
to compare hash keys (but not values).
If you leave eql?
undefined, then
hashes will compare instances of your class by object identity. This
means that if you associate a value with a key p
, you will only be able to retrieve that
value with the exact same object p
.
An object q
won’t work, even if
p == q
. Mutable objects do not work
well as hash keys, but leaving eql?
undefined neatly sidesteps the problem. (See Hash Codes, Equality, and Mutable Keys for more on hashes and mutable keys.)
Because eql?
is used for
hashes, you must never implement this method by itself. If you define
an eql?
method, you must also
define a hash
method to compute a
hashcode for your object. If two objects are equal according to
eql?
, then their hash
methods must
return the same value. (Two unequal objects may return the same
hashcode, but you should avoid this to the extent possible.)
Implementing optimal hash
methods can be very tricky. Fortunately, there is a simple way to
compute perfectly adequate hashcodes for just about any class: simply
combine the hashcodes of all the objects referenced by your class.
(More precisely: combine the hashcodes of all the objects compared by
your eql?
method.) The trick is to
combine the hashcodes in the proper way. The following hash
method is not a
good one:
def hash @x.hash + @y.hash end
The problem with this method is that it returns the same
hashcode for the point (1,0)
as it
does for the point (0,1)
. This is
legal, but it leads to poor performance when points are used as hash
keys. Instead, we should mix things up a bit:
def hash code = 17 code = 37*code + @x.hash code = 37*code + @y.hash # Add lines like this for each significant instance variable code # Return the resulting code end
This general-purpose hashcode recipe should be suitable for most
Ruby classes. It, and its constants 17
and 37
, are adapted from the book
Effective Java by Joshua Bloch (Prentice
Hall).
Suppose we wish to define an ordering for Point
objects so that we can compare them
and sort them. There are a number of ways to order points, but we’ll
chose to arrange them based on their distance from the origin. This
distance (or magnitude) is computed by the Pythagorean theorem: the
square root of the sum of the squares of the X and Y
coordinates.
To define this ordering for Point
objects, we need only define the
<=>
operator (see Comparison: <, <=, >, >=, and <=>) and include the Comparable
module. Doing this mixes in
implementations of the equality and relational operators that are
based on our implementation of the general <=>
operator we defined. The <=>
operator should compare self
to the object it is passed. If self
is less than that object (closer to the
origin, in this case), it should return –1
. If the two objects are equal, it should
return 0
. And if self
is greater than the argument object,
the method should return 1
. (The
method should return nil
if the
argument object and self
are of
incomparable types.) The following code is our implementation of
<=>
. There are two things to note about it. First, it doesn’t
bother with the Math.sqrt
method
and instead simply compares the sum of the squares of the coordinates.
Second, after computing the sums of the squares, it simply delegates
to the <=>
operator of the
Float
class:
include Comparable # Mix in methods from the Comparable module. # Define an ordering for points based on their distance from the origin. # This method is required by the Comparable module. def <=>(other) return nil unless other.instance_of? Point @x**2 + @y**2 <=> other.x**2 + other.y**2 end
Note that the Comparable
module defines an ==
method that
uses our definition of <=>
.
Our distance-based comparison operator results in an ==
method that considers the points (1,0)
and (0,1)
to be equal. Because our Point
class explicitly defines its own
==
method, however, the ==
method of Comparable
is never invoked. Ideally, the
==
and <=>
operators should have consistent
definitions of equality. This was not possible in our Point
class, and we end up with operators
that allow the following:
p,q = Point.new(1,0), Point.new(0,1) p == q # => false: p is not equal to q p < q # => false: p is not less than q p > q # => false: p is not greater than q
Finally, It is worth noting here that the Enumerable
module defines several methods,
such as sort
, min
, and max
, that only work if the objects being
enumerated define the <=>
operator.
The Point
class we’ve been
developing is immutable: once a point object has
been created, there is no public API to change the X and Y coordinates
of that point. This is probably as it should be. But let’s detour and
investigate some methods we might add if we wanted points to be mutable.
First of all, we’d need x=
and y=
setter methods to allow the
X and Y coordinates to be set directly. We could define these methods
explicitly, or simply change our attr_reader
line to attr_accessor
:
attr_accessor :x, :y
Next, we’d like an alternative to the +
operator for when we want to add the
coordinates of point q
to the
coordinates of point p
, and modify
point p
rather than creating and
returning a new Point
object. We’ll
call this method add!
, with the
exclamation mark indicating that it alters the internal state of the
object on which it is invoked:
def add!(p) # Add p to self, return modified self @x += p.x @y += p.y self end
When defining a mutator method, we normally only add an
exclamation mark to the name if there is a nonmutating version of the
same method. In this case, the name add!
makes sense if we also define an
add
method that returns a new
object, rather than altering its receiver. A nonmutating version of a
mutator method is often written simply by creating a copy of self
and invoking the mutator on the copied
object:
def add(p) # A nonmutating version of add! q = self.dup # Make a copy of self q.add!(p) # Invoke the mutating method on the copy end
In this trivial example, our add
method works just like the +
operator we’ve already defined, and it’s
not really necessary. So if we don’t define a nonmutating add
, we should consider dropping the
exclamation mark from add!
and
allowing the name of the method itself (“add” instead of “plus”) to
indicate that it is a mutator.
If you want a mutable Point
class, one way to create it is with Struct
. Struct
is a core Ruby class that generates
other classes. These generated classes have accessor methods for the
named fields you specify. There are two ways to create a new class
with Struct.new
:
Struct.new("Point", :x, :y) # Creates new class Struct::Point Point = Struct.new(:x, :y) # Creates new class, assigns to Point
Once a class has been created with Struct.new
, you can use it like any other
class. Its new
method will expect
values for each of the named fields you specify, and its instance
methods provide read and write accessors for those fields:
p = Point.new(1,2) # => #<struct Point x=1, y=2> p.x # => 1 p.y # => 2 p.x = 3 # => 3 p.x # => 3
Structs also define the []
and []=
operators for array and
hash-style indexing, and even provide each
and each_pair
iterators for looping through the
values held in an instance of the struct:
p[:x] = 4 # => 4: same as p.x = p[:x] # => 4: same as p.x p[1] # => 2: same as p.y p.each {|c| print c} # prints "42" p.each_pair {|n,c| print n,c } # prints "x4y2"
Struct-based classes have a working ==
operator, can be used as hash keys
(though caution is necessary because they are mutable), and even
define a helpful to_s
method:
q = Point.new(4,2) q == p # => true h = {q => 1} # Create a hash using q as a key h[p] # => 1: extract value using p as key q.to_s # => "#<struct Point x=4, y=2>"
A Point
class defined as a
struct does not have point-specific methods like add!
or the <=>
operator defined earlier in this
chapter. There is no reason we can’t add them, though. Ruby class
definitions are not static. Any class (including classes defined with
Struct.new
) can be “opened” and
have methods added to it. Here’s a Point
class initially defined as a Struct
, with point-specific methods
added:
Point = Struct.new(:x, :y) # Create new class, assign to Point class Point # Open Point class for new methods def add!(other) # Define an add! method self.x += other.x self.y += other.y self end include Comparable # Include a module for the class def <=>(other) # Define the <=> operator return nil unless other.instance_of? Point self.x**2 + self.y**2 <=> other.x**2 + other.y**2 end end
As noted at the beginning of this section, the Struct
class is designed to create mutable
classes. With just a bit of work, however, we can make a Struct
-based class immutable:
Point = Struct.new(:x, :y) # Define mutable class class Point # Open the class undef x=,y=,[]= # Undefine mutator methods end
Let’s take another approach to adding Point
objects together. Instead of invoking
an instance method of one point and passing another point to that
method, let’s write a method named sum
that takes any number of Point
objects, adds them together, and
returns a new Point
. This method is
not an instance method invoked on a Point
object. Rather, it is a class method, invoked
through the Point
class itself. We
might invoke the sum
method like
this:
total = Point.sum(p1, p2, p3) # p1, p2 and p3 are Point objects
Keep in mind that the expression Point
refers to a Class
object that represents our point
class. To define a class method for the Point
class, what we are really doing is
defining a singleton method of the Point
object. (We covered singleton methods
in Defining Singleton Methods.) To define a singleton method,
use the def
statement as usual, but
specify the object on which the method is to be defined as well as the
name of the method. Our class method sum
is defined like this:
class Point attr_reader :x, :y # Define accessor methods for our instance variables def Point.sum(*points) # Return the sum of an arbitrary number of points x = y = 0 points.each {|p| x += p.x; y += p.y } Point.new(x,y) end # ...the rest of class omitted here... end
This definition of the class method names the class explicitly,
and mirrors the syntax used to invoke the method. Class methods can
also be defined using self
instead
of the class name. Thus, this method could also be written like
this:
def self.sum(*points) # Return the sum of an arbitrary number of points x = y = 0 points.each {|p| x += p.x; y += p.y } Point.new(x,y) end
Using self
instead of Point
makes the
code slightly less clear, but it’s an application of the DRY (Don’t
Repeat Yourself) principle. If you use self
instead of the class name, you can
change the name of a class without having to edit the definition of
its class methods.
There is yet another technique for defining class methods. Though it is less clear than the previously shown technique, it can be handy when defining multiple class methods, and you are likely to see it used in existing code:
# Open up the Point object so we can add methods to it class << Point # Syntax for adding methods to a single object def sum(*points) # This is the class method Point.sum x = y = 0 points.each {|p| x += p.x; y += p.y } Point.new(x,y) end # Other class methods can be defined here end
This technique can also be used inside the class definition,
where we can use self
instead of
repeating the class name:
class Point # Instance methods go here class << self # Class methods go here end end
We’ll learn more about this syntax in Singleton Methods and the Eigenclass.
Many classes can benefit from the definition of some associated
constants. Here are some constants that might be useful for our
Point
class:
class Point def initialize(x,y) # Initialize method @x,@y = x, y end ORIGIN = Point.new(0,0) UNIT_X = Point.new(1,0) UNIT_Y = Point.new(0,1) # Rest of class definition goes here end
Inside the class definition, these constants can be referred to by their unqualified names. Outside the definition, they must be prefixed by the name of the class, of course:
Point::UNIT_X + Point::UNIT_Y # => (1,1)
Note that because our constants in this example refer to
instances of the class, we cannot define the constants until after
we’ve defined the initialize
method of the class. Also, keep in mind that it is perfectly
legal to define constants in the Point
class from outside the class:
Point::NEGATIVE_UNIT_X = Point.new(-1,0)
Class variables are visible to, and shared by, the class
methods and the instance methods of a class, and also by the class
definition itself. Like instance variables, class variables are
encapsulated; they can be used by the implementation of a class, but
they are not visible to the users of a class. Class variables have
names that begin with @@
.
There is no real need to use class variables in our Point
class, but for the purposes of this
tutorial, let’s suppose that we want to collect data about the number
of Point
objects that are created
and their average coordinates. Here’s how we might write the
code:
class Point # Initialize our class variables in the class definition itself @@n = 0 # How many points have been created @@totalX = 0 # The sum of all X coordinates @@totalY = 0 # The sum of all Y coordinates def initialize(x,y) # Initialize method @x,@y = x, y # Sets initial values for instance variables # Use the class variables in this instance method to collect data @@n += 1 # Keep track of how many Points have been created @@totalX += x # Add these coordinates to the totals @@totalY += y end # A class method to report the data we collected def self.report # Here we use the class variables in a class method puts "Number of points created: #@@n" puts "Average X coordinate: #{@@totalX.to_f/@@n}" puts "Average Y coordinate: #{@@totalY.to_f/@@n}" end end
The thing to notice about this code is that class variables are
used in instance methods, class methods, and in the class definition
itself, outside of any method. Class variables are fundamentally
different than instance variables. We’ve seen that instance variables
are always evaluated in reference to self
. That is why an instance variable
reference in a class definition or class method is completely
different from an instance variable reference in an instance method.
Class variables, on the other hand, are always evaluated in reference
to the class object created by the enclosing class
definition statement.
Classes are objects and can have instance variables just as other objects can. The instance variables of a class—often called class instance variables—are not the same as class variables. But they are similar enough that they can often be used instead of class variables.
An instance variable used inside a class
definition but outside an instance
method definition is a class instance variable. Like class variables,
class instance variables are associated with the class rather than
with any particular instance of the class. A disadvantage of class instance variables
is that they cannot be used within instance methods as class variables
can. Another disadvantage is the potential for confusing them with
ordinary instance variables. Without the distinctive punctuation
prefixes, it may be more difficult to remember whether a variable is
associated with instances or with the class object.
One of the most important advantages of class instance variables over class variables has to do with the confusing behavior of class variables when subclassing an existing class. We’ll return to this point later in the chapter.
Let’s port our statistics-gathering version of the Point
class to use class instance variables
instead of class variables. The only difficulty is that because class
instance variables cannot be used from instance methods, we must move
the statistics gathering code out of the initialize
method
(which is an instance method) and into the new
class method used to create
points:
class Point # Initialize our class instance variables in the class definition itself @n = 0 # How many points have been created @totalX = 0 # The sum of all X coordinates @totalY = 0 # The sum of all Y coordinates def initialize(x,y) # Initialize method @x,@y = x, y # Sets initial values for instance variables end def self.new(x,y) # Class method to create new Point objects # Use the class instance variables in this class method to collect data @n += 1 # Keep track of how many Points have been created @totalX += x # Add these coordinates to the totals @totalY += y super # Invoke the real definition of new to create a Point # More about super later in the chapter end # A class method to report the data we collected def self.report # Here we use the class instance variables in a class method puts "Number of points created: #@n" puts "Average X coordinate: #{@totalX.to_f/@n}" puts "Average Y coordinate: #{@totalY.to_f/@n}" end end
Because class instance variables are just instance variables of
class objects, we can use attr
,
attr_reader
, and attr_accessor
to create accessor methods for
them. The trick, however, is to invoke these metaprogramming methods
in the right context. Recall that one way to define class methods uses
the syntax class << self
.
This same syntax allows us to define attribute accessor methods for
class instance variables:
class << self attr_accessor :n, :totalX, :totalY end
With these accessors defined, we can refer to our raw data as
Point.n
, Point.totalX
, and Point.totalY
.
18.226.185.87