Chapter 7. Classes and Modules
Ruby is an object-oriented language in a very pure sense: every value
in Ruby is (or at least behaves like) an object. Every object is an instance
of a class. A class defines a set of methods that an object responds to.
Classes may extend or subclass other classes, and inherit or override the
methods of their superclass. Classes can also include—or inherit methods
from—modules.
Ruby's objects are strictly encapsulated: their state can be accessed
only through the methods they define. The instance variables manipulated by
those methods cannot be directly accessed from outside of the object. It is
possible to define getter and setter accessor methods that appear to access
object state directly. These pairs of accessor methods are known as attributes and are distinct from
instance variables. The methods defined by a class may have "public,"
"protected," or "private" visibility, which affects how and where they may
be invoked.
In contrast to the strict encapsulation of object state, Ruby's
classes are very open. Any Ruby program can add methods to existing classes,
and it is even possible to add "singleton methods" to individual
objects.
Much of Ruby's OO architecture is part of the core language. Other
parts, such as the creation of attributes and the declaration of method
visibility, are done with methods rather than true language keywords. This
chapter begins with an extended tutorial that demonstrates how to define a
class and add methods to it. This tutorial is followed by sections on more
advanced topics, including:
Method visibility
Subclassing and inheritance
Object creation and initialization
Modules, both as namespaces and as includable "mixins"
Singleton methods and the eigenclass
The method name resolution algorithm
The constant name resolution algorithm
7.1. Defining a Simple Class
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
7.1.1. Creating the Class
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.
7.1.2. Instantiating a Point
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
7.1.3. Initializing a Point
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.
The instance variables of an object can only be accessed by the instance methods of that object. Code that is not inside an instance method cannot read or set the value of an instance variable (unless it uses one of the reflective techniques that are described in Chapter 8). |
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.
7.1.4. Defining a to_s Method
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 = new Point(1,2) # Create a new Point object
puts p # Displays "(1,2)"
7.1.5. Accessors and Attributes
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
Once you've defined a setter method like x= for your class, you might be tempted to use it within other instance methods of your class. That is, instead of writing @x=2, you might write x=2, intending to invoke x=(2) implicitly on self. It doesn't work, of course; x=2 simply creates a new local variable. This is a not-uncommon mistake for novices who are just learning about setter methods and assignment in Ruby. The rule is that assignment expressions will only invoke a setter method when invoked through an object. If you want to use a setter from within the class that defines it, invoke it explicitly through self. For example: self.x=2. |
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 method 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. 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.
7.1.6. Defining Operators
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.)
Our + method does not do any type checking; it simply assumes that it has been passed a suitable object. It is fairly common in Ruby programming to be loose about the definition of "suitable." In the case of our + method, any object that has methods named x and y will do, as long as those methods expect no arguments and return a number of some sort. We don't care if the argument actually is a point, as long as it looks and behaves like a point. This approach is sometimes called "duck typing," after the adage "if it walks like a duck and quacks like a duck, it must be a duck." If we pass an object to + that is not suitable, Ruby will raise an exception. Attempting to add 3 to a point, for example, results in this error message: NoMethodError: undefined method `x' for 3:Fixnum from ./point.rb:37:in `+'
Translated, this tells us that the Fixnum 3 does not have a method named x, and that this error arose in the + method of the Point class. This is all the information we need to figure out the source of the problem, but it is somewhat obscure. Checking the class of method arguments may make it easier to debug code that uses that method. Here is a version of the method with class verification: def +(other) raise TypeError, "Point argument expected" unless other.is_a? Point Point.new(@x + other.x, @y + other.y) end
Here is a looser version of type checking that provides improved error messages but still allows duck typing: def +(other) raise TypeError, "Point-like argument expected" unless other.respond_to? :x and other.respond_to? :y Point.new(@x + other.x, @y + other.y) end
Note that this version of the method still assumes that the x and y methods return numbers. We'd get an obscure error message if one of these methods returned a string, for example. Another approach to type checking occurs after the fact. We can simply handle any exceptions that occur during execution of the method and raise a more appropriate exception of our own: def +(other) # Assume that other looks like a Point Point.new(@x + other.x, @y + other.y) rescue # If anything goes wrong above raise TypeError, # Then raise our own exception "Point addition with an argument that does not quack like a Point!" end
|
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 Section 3.8.7.4 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
7.1.7. Array and Hash Access with [ ]
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
7.1.8. Enumerating Coordinates
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
7.1.9. Point Equality
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
Section 3.8.5 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.
elsif # If o is not a Point
false # then, by definition, self != o.
end
end
The + operator we defined earlier did no type checking at all: it works with any argument object with x and y methods that return numbers. This == method is implemented differently; instead of allowing duck typing, it requires that the argument is a Point. This is an implementation choice. The implementation of == above chooses to define equality so that an object cannot be equal to a Point unless it is itself a Point. Implementations may be stricter or more liberal than this. The implementation above uses the is_a? predicate to test the class of the argument. This allows an instance of a subclass of Point to be equal to a Point. A stricter implementation would use instance_of? to disallow subclass instances. Similarly, the implementation above uses == to compare the X and Y coordinates. For numbers, the == operator allows type conversion, which means that the point (1,1) is equal to (1.0,1.0). This is probably as it should be, but a stricter definition of equality could use eql? to compare the coordinates. A more liberal definition of equality would support duck typing. Some caution is required, however. Our == method should not raise a NoMethodError if the argument object does not have x and y methods. Instead, it should simply return false: def ==(o) # Is self == o? @x == o.x && @y == o.y # Assume o has proper x and y methods rescue # If that assumption fails false # Then self != o end
|
Recall from Section 3.8.5 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)
elsif
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
Section 3.4.2 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).
7.1.10. Ordering Points
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 Section 4.6.6) 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.
7.1.11. A Mutable Point
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.
7.1.12. Quick and Easy Mutable Classes
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
The second line in the code relies on a curious fact about Ruby classes: if you assign an unnamed class object to a constant, the name of that constant becomes the name of a class. You can observe this same behavior if you use the Class.new constructor: C = Class.new # A new class with no body, assigned to a constant c = C.new # Create an instance of the class c.class.to_s # => "C": constant name becomes class name
|
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
7.1.13. A Class Method
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 Section 6.1.4.) 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:
Code View:
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 Section 7.7.
7.1.14. Constants
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)
7.1.15. Class Variables
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.
7.1.16. Class Instance Variables
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:
Code View:
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.