Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (5.14 MB, 519 trang )
Class/module design and naming
4.4.1
111
Mix-ins and/or inheritance
Module mix-ins are closely related to class inheritance. In both cases, one entity (class
or module) is establishing a close connection with another, by becoming neighbors
on a method-lookup path. In some cases, you may find that you can design part of
your program either with modules or with inheritance.
Our CargoHold class is an example. We implemented it by having it mix in the
Stacklike module. But had we gone the route of writing a Stack class instead of a
Stacklike module, we still could have had a CargoHold. It would have been a subclass
of Stack, as illustrated in listing 4.14.
Listing 4.14
CargoHold, inheriting from Stack instead of mixing in Stacklike
class Stack
attr_reader :stack
def initialize
@stack = []
end
def add_to_stack(obj)
@stack.push(obj)
end
def take_from_stack
@stack.pop
end
end
class Suitcase
end
class CargoHold < Stack
def load_and_report(obj)
print "Loading object "
puts obj.object_id
add_to_stack(obj)
end
def unload
take_from_stack
end
end
From the point of view of an individual CargoHold object, the process works in
listing 4.14 exactly as it worked in the earlier implementation, where CargoHold
mixed in the Stacklike module. The object is concerned with finding and executing
methods that correspond to the messages it receives. It either finds such methods on
its method-lookup path, or it doesn’t. It doesn’t care whether the methods were
defined in a module or a class. It’s like searching a house for a screwdriver: you don’t
care which room you find it in, and which room you find it in makes no difference to
what happens when you subsequently employ the screwdriver for a task.
There’s nothing wrong with this inheritance-based approach to implementing
CargoHold, except that it eats up the one inheritance opportunity CargoHold has. If
another class might be more suitable than Stack to serve as CargoHold’s superclass
Licensed to sam kaplan
112
CHAPTER 4
Modules and program organization
(like, hypothetically, StorageSpace or AirplaneSection), we might end up needing
the flexibility we’d gain by turning at least one of those classes into a module.
No single rule or formula always results in the right design. But it’s useful to keep a
couple of considerations in mind when you’re making class-versus-module decisions:
■
■
Modules don’t have instances. It follows that entities or things are generally best
modeled in classes, and characteristics or properties of entities or things are
best encapsulated in modules. Correspondingly, as noted in section 4.1.1, class
names tend to be nouns, whereas module names are often adjectives (Stack
versus Stacklike).
A class can have only one superclass, but it can mix in as many modules as it wants. If
you’re using inheritance, give priority to creating a sensible superclass/subclass
relationship. Don’t use up a class’s one and only superclass relationship to
endow the class with what might turn out to be just one of several sets of characteristics.
Summing up these rules in one example, here is what you should not do:
module Vehicle
...
class SelfPropelling
...
class Truck < SelfPropelling
include Vehicle
...
Rather, you should do this:
module SelfPropelling
...
class Vehicle
include SelfPropelling
...
class Truck < Vehicle
...
The second version models the entities and properties much more neatly. Truck
descends from Vehicle (which makes sense), whereas SelfPropelling is a characteristic of vehicles (at least, all those we care about in this model of the world)—a characteristic that is passed on to trucks by virtue of Truck being a descendant, or specialized
form, of Vehicle.
Another important consideration in class/module design is the nesting of modules and/or classes inside each other.
4.4.2
Nesting modules and classes
You can nest a class definition inside a module definition, like this:
module Tools
class Hammer
Licensed to sam kaplan
Summary
113
To create an instance of the Hammer class defined inside the Tools module, you use the
double-colon constant lookup token (::) to point the way to the name of the class:
h = Tools::Hammer.new
Nested module/class chains like Tools::Hammer are sometimes used to create separate namespaces for classes, modules, and methods. This technique can help if two
classes have a similar name but aren’t the same class. For example, if you have a
Tool::Hammer class, you can also have a Piano::Hammer class, and the two Hammer
classes won’t conflict with each other because they’re nested in their own namespace
(Tool in one case, Piano in the other).
(An alternative way to achieve this separation would be to have a ToolsHammer class
and a PianoHammer class, without bothering to nest them in modules. But stringing
names together like that can quickly lead to visual clutter, especially when elements
are nested deeper than two levels.)
Class or module?
When you see a construct like Tools::Hammer, you can’t tell solely from that construct what’s a class and what’s a module—nor, for that matter, whether Hammer is
a plain old constant. (Tools has to be a class or module, because it’s got Hammer
nested inside it.) In many cases, the fact that you can’t tell classes from modules in
this kind of context doesn’t matter; what matters is the nesting or chaining of names
in a way that makes sense. That’s just as well, because you can’t tell what’s what
without looking at the source code or the documentation. This is a consequence of
the fact that classes are modules—the class Class is a subclass of the class
Module—and in many respects (with the most notable exception that classes can be
instantiated), their behavior is similar. Of course, normally you’d know, either
because you wrote the code or because you’ve seen documentation. Still, it pays to
realize that the notation itself doesn’t tell you everything.
We’ll look further at nested classes, modules, and other constants in the next chapter,
when we talk in more detail about the subject of scope. Meanwhile, note that this ability to nest modules and classes inside each other (to any depth, in any order) gives you
yet another axis along which you can plan your program’s design and structure.
4.5
Summary
This chapter has been both a companion to and a continuation of the previous chapter on classes. We’ve looked in detail at modules, which are similar to classes in that
they bundle methods and constants together, but which can’t be instantiated. You’ve
seen examples of how you might use modules to express the design of a program.
We’ve taken an object’s-eye view of the process of finding and executing a method in
response to a message, or handling failure with method_missing in cases where the
message doesn't match a method. We’ve also looked at some techniques you can
use—including nesting classes and modules inside each other, which can have the
Licensed to sam kaplan
114
CHAPTER 4
Modules and program organization
benefit of keeping namespaces separate and clear. Finally, we discussed aspects of
modular organization, including nesting of modules and classes, and how to keep
things clear as you start to have more classes and modules in your code.
It’s particularly important to take on board the way that objects resolve messages
into methods: they go on a search through a succession of classes and modules.
Objects don’t, themselves, have methods, even though phrasing it that way is sometimes a handy shortcut. Classes and modules have methods; objects have the ability to
traverse classes and modules in search of methods.
Now that we’re nesting elements inside each other, the next topic we should and
will examine in detail is scope: what happens to data and variables when your program
moves from one code context to another. We’ll look at scope in conjunction with the
related, often interwoven topic of self, the default object.
Licensed to sam kaplan
The default object (self),
scope, and visibility
In this chapter
■
The role of the current or default object, self
■
Scoping rules for local, global, and class
variables
■
Constant lookup and visibility
■
Method access rules
In describing and discussing computer programs, we often use spatial and, sometimes, human metaphors. We talk about being “in” a class definition or returning
“from” a method call. We address objects in the second person, as in
obj.respond_to?("x") (that is, “Hey obj, do you respond to ‘x’?”). As a program
runs, the question of which objects are being addressed, and where in the imaginary space of the program they stand, constantly shifts.
And the shifts aren’t just metaphorical. The meanings of identifiers shift too. A
few elements mean the same thing everywhere. Integers, for example, mean what
they mean wherever you see them. The same is true for keywords: you can’t use
115
Licensed to sam kaplan
116
CHAPTER 5
The default object (self), scope, and visibility
keywords like def and class as variable names, so when you see them, you can easily
glean what they’re doing. But most elements depend on context for their meaning.
Most words and tokens—most identifiers—can mean different things at different
places and times.
This chapter is about orienting yourself in Ruby code: knowing how the identifiers
you’re using are going to resolve, following the shifts in context, and making sense of
the use and reuse of identifiers and terms. If you understand what can change from
one context to another, and also what triggers a change in context (for example, entering a method-definition block), you can always get your bearings in a Ruby program.
And it’s not just a matter of passive Ruby literacy: you also need to know about contexts
and how they affect the meaning of what you’re doing when you’re writing Ruby.
This chapter focuses primarily on two topics: scope and self. The rules of scope
govern the visibility of variables (and other elements, but largely variables). It’s important to know what scope you’re in, so that you can tell what the variables refer to and
not confuse them with variables from different scopes that have the same name, nor
with similarly named methods.
Unlike scope, self isn’t so much a concept as an object. But self changes in the
course of a program. At every moment, only one object is playing the role of self. But
it’s not necessarily the same object from one moment to the next. The self in Ruby is
like the first person or “I” of the program. As in a book with multiple first-person narrators, the I role can get passed around. There’s always one self, but who it is—what
object it is—will vary.
Between them, self and scope are the master keys to orienting yourself in a Ruby
program. If you know what scope you’re in and know what object is self, you’ll be able
to tell what’s going on, and you’ll be able to analyze errors quickly.
The third subtopic of this chapter is method access. Ruby provides mechanisms for
making distinctions among access levels of methods. Basically, this means rules limiting the calling of methods depending on what self is. Method access is therefore a
meta-topic, grounded in the study of self and scope.
Finally, we’ll also discuss a topic that pulls together several of these threads: toplevel methods, which are written outside of any class or module definition.
Let’s start with self.
5.1
Understanding self, the current/default object
One of the cornerstones of Ruby programming—the backbone, in some respects—is
the default object or current object, accessible to you in your program through the
keyword self. At every point when your program is running, there is one and only
one self. Being self has certain privileges, as you’ll see. In this section, we’ll look at
how Ruby determines which object is self at a given point and what privileges are
granted to the object that is self.
Licensed to sam kaplan
117
Understanding self, the current/default object
5.1.1
Who gets to be self, and where
There is always one (and only one) current object or self. You can tell which object it
is by following a small set of rules. These rules are summarized in table 5.1; the table’s
contents will be explained and illustrated as we go along.
To know which object is self, you need to know what context you’re in. In practice,
there aren’t many contexts to worry about. There’s the top level (before you’ve
entered any other context, such as a class definition). There are class-definition
blocks, module-definition blocks, and method-definition blocks. Aside from a few subtleties in the way these contexts interact, that’s about it. As shown in table 5.1, self is
determined by which of these contexts you’re in (class and module definitions are
similar and closely related).
Figure 5.1 gives you a diagrammatic view of most of the cases in table 5.1. Both
show you that some object is always self and that which object is self depends on where
you are in the program.
Table 5.1
How the current object (self) is determined
Context
Example
Which object is self?
Top level of program
Any code outside of other blocks
main (built-in top-level default
object)
Class definition
class C
self
The class object C
Module definition
module M
self
The module object M
Method definitions
1. Top level (outside any definition block)
Whatever object is self when
the method is called; top-level
methods are available as private methods to all objects
def method_name
self
2. Instance method definition
in class
An instance of C, responding
to method_name
class C
def method_name
self
3. Instance method definition
in module
module M
def method_name
self
4. Singleton method on specific
object
I. Individual object extended
by M
II. Instance of class that
mixes in M
obj
def obj.method_name
self
Licensed to sam kaplan