|
|||
Previous < |
Contents ^
|
Next >
|
Song
,[As we
mentioned on page 9, class names start with an
uppercase letter, while method names start with a lowercase letter.]
which contains just a single method, initialize
.
class Song def initialize(name, artist, duration) @name = name @artist = artist @duration = duration end end |
initialize
is a special method in Ruby programs. When you
call Song.new
to create a new Song
object, Ruby creates an
uninitialized object and then calls that object's initialize
method, passing in any parameters that were passed to
new
. This gives you a chance to write code that sets up your
object's state.
For class Song
, the initialize
method takes three
parameters. These parameters act just like local variables within the
method, so they follow the local variable naming convention of
starting with a lowercase letter.
Each object represents its own song, so we need each of our Song
objects to carry around its own song name, artist, and duration. This
means we need to store these values as instance variables
within the object.
In Ruby, an instance variable is simply a name
preceded by an ``at'' sign (``@''). In our example, the parameter
name
is assigned to the instance variable @name
,
artist
is assigned to @artist
, and duration
(the
length of the song in seconds) is assigned to @duration
.
Let's test our spiffy new class.
aSong = Song.new("Bicylops", "Fleck", 260)
|
||
aSong.inspect
|
» |
"#<Song:0x401b4924 @duration=260, @artist=\"Fleck\", @name=\"Bicylops\">"
|
inspect
message,
which can be sent to any object, dumps out the object's id and instance
variables. It looks as though we have them set up correctly.
Our experience tells us that during development we'll be printing out
the contents of a Song
object many times, and inspect
's
default formatting leaves something to be desired. Fortunately, Ruby
has a standard message, to_s
,
which it
sends to any object it wants to render as a string. Let's try it on
our song.
aSong = Song.new("Bicylops", "Fleck", 260)
|
||
aSong.to_s
|
» |
"#<Song:0x401b499c>"
|
to_s
in our class.
As we do this, we should also take a moment to talk about how we're
showing the class definitions in this book.
In Ruby, classes are never closed: you can always add methods to an
existing class.
This applies to the classes you write as well as the
standard, built-in classes. All you have to do is open up a class
definition for an existing class, and the new contents you specify
will be added to whatever's there.
This is great for our purposes. As we go through this chapter, adding
features to our classes, we'll show just the class definitions for the
new methods; the old ones will still be there. It saves us having to
repeat redundant stuff in each example. Obviously, though, if you were
creating this code from scratch, you'd probably just throw all the
methods into a single class definition.
Enough detail! Let's get back to adding a to_s
method to our
Song
class.
class Song
|
||
def to_s
|
||
"Song: #{@name}--#{@artist} (#{@duration})"
|
||
end
|
||
end
|
||
aSong = Song.new("Bicylops", "Fleck", 260)
|
||
aSong.to_s
|
» |
"Song: Bicylops--Fleck (260)"
|
to_s
for all objects, but
we didn't say how. The answer has to do with inheritance, subclassing,
and how Ruby determines what method to run when you send a message to
an object. This is a subject for a new section, so....
Song
. Then
marketing comes along and tells us that we need to provide karaoke
support. A karaoke song is just like any other (there's no vocal on
it, but that doesn't concern us). However, it also has an associated
set of lyrics, along with timing information. When our jukebox plays a
karaoke song, the lyrics should flow across the screen on the front of
the jukebox in time with the music.
An approach to this problem is to define a new class, KaraokeSong
,
which is just like Song
, but with a lyric track.
class KaraokeSong < Song def initialize(name, artist, duration, lyrics) super(name, artist, duration) @lyrics = lyrics end end |
< Song
'' on the class definition line tells Ruby that a
KaraokeSong
is a subclass of Song
.
(Not surprisingly,
this means that Song
is a superclass of KaraokeSong
. People
also talk about parent-child relationships, so KaraokeSong
's
parent would be Song
.) For now, don't worry too much about the
initialize
method; we'll talk about that super
call later.
Let's create a KaraokeSong
and check that our code worked. (In the
final system, the lyrics will be held in an object that includes the
text and timing information. To test out our class, though, we'll just
use a string. This is another benefit of untyped languages---we don't
have to define everything before we start running code.
aSong = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the...")
|
||
aSong.to_s
|
» |
"Song: My Way--Sinatra (225)"
|
to_s
method show the
lyric?
The answer has to do with the way Ruby determines which method should
be called when you send a message to an object. When Ruby compiles the
method invocation aSong.to_s
, it doesn't actually know where to
find the method to_s
. Instead, it defers the decision until
the program is run. At that time, it looks at the class of aSong
.
If that class implements a method with the same name as the message,
that method is run. Otherwise, Ruby looks for a method in the parent
class, and then in the grandparent, and so on up the ancestor chain.
If it runs out of ancestors without finding the appropriate method, it
takes a special action that normally results in an error being
raised.[In fact, you can intercept this error, which allows
you to fake out methods at runtime. This is described under
Object#method_missing
on page 355.]
So, back to our example. We sent the message to_s
to
aSong
, an object of class KaraokeSong
.
Ruby looks in
KaraokeSong
for a method called to_s
, but doesn't find
it. The interpreter then looks in KaraokeSong
's parent, class
Song
, and there it finds the to_s
method that we defined
on page 18. That's why it prints out the song details but
not the lyrics---class Song
doesn't know anything about lyrics.
Let's fix this by implementing KaraokeSong#to_s
. There are a
number of ways to do this. Let's start with a bad way. We'll copy
the to_s
method from Song
and add on the lyric.
class KaraokeSong
|
||
# ...
|
||
def to_s
|
||
"KS: #{@name}--#{@artist} (#{@duration}) [#{@lyrics}]"
|
||
end
|
||
end
|
||
aSong = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the...")
|
||
aSong.to_s
|
» |
"KS: My Way--Sinatra (225) [And now, the...]"
|
@lyrics
instance
variable. To do this, the subclass directly accesses the instance
variables of its ancestors. So why is this a bad way to implement
to_s
?
The answer has to do with good programming style (and something called
decoupling). By poking around in our parent's internal state,
we're tying ourselves tightly to its implementation. Say we decided to
change Song
to store the duration in milliseconds. Suddenly,
KaraokeSong
would start reporting ridiculous values. The idea of a
karaoke version of ``My Way'' that lasts for 3750 minutes is just too
frightening to consider.
We get around this problem by having each class handle its own
internal state. When KaraokeSong#to_s
is called, we'll have it call
its parent's to_s
method to get the song details. It will
then append to this the lyric information and return the result. The
trick here is the Ruby keyword ``super
''. When you invoke
super
with no arguments, Ruby sends a message to the current
object's parent, asking it to invoke a method of the same name as the
current method, and passing it the parameters that were passed to the
current method. Now we can implement our new and improved
to_s
.
class KaraokeSong < Song
|
||
# Format ourselves as a string by appending
|
||
# our lyrics to our parent's #to_s value.
|
||
def to_s
|
||
super + " [#{@lyrics}]"
|
||
end
|
||
end
|
||
aSong = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the...")
|
||
aSong.to_s
|
» |
"Song: My Way--Sinatra (225) [And now, the...]"
|
KaraokeSong
was a subclass of
Song
, but we didn't specify a parent class for Song
itself. If
you don't specify a parent when defining a class, Ruby supplies
class Object
as a default. This means that all objects have
Object
as an ancestor, and that Object
's instance methods are
available to every object in Ruby. Back on page 18 we said
that to_s
is available to all objects. Now we know why;
to_s
is one of more than 35 instance methods in
class Object
. The complete list begins on page 351.
Song
.
Song
objects we've created so far have an internal state (such as
the song title and artist). That state is private to those
objects---no other object can access an object's instance variables.
In general, this is a Good Thing. It means that the object is solely
responsible for maintaining its own consistency.
However, an object that is totally secretive is pretty useless---you
can create it, but then you can't do anything with it. You'll normally
define methods that let you access and manipulate the state of an
object, allowing the outside world to interact with the object. These
externally visible facets of an object are called its
attributes.
For our Song
objects, the first thing we may need is the ability
to find out the title and artist (so we can display them while the
song is playing) and the duration (so we can display some kind of
progress bar).
class Song
|
||
def name
|
||
@name
|
||
end
|
||
def artist
|
||
@artist
|
||
end
|
||
def duration
|
||
@duration
|
||
end
|
||
end
|
||
aSong = Song.new("Bicylops", "Fleck", 260)
|
||
aSong.artist
|
» |
"Fleck"
|
aSong.name
|
» |
"Bicylops"
|
aSong.duration
|
» |
260
|
attr_reader
creates these
accessor methods for you.
class Song
|
||
attr_reader :name, :artist, :duration
|
||
end
|
||
aSong = Song.new("Bicylops", "Fleck", 260)
|
||
aSong.artist
|
» |
"Fleck"
|
aSong.name
|
» |
"Bicylops"
|
aSong.duration
|
» |
260
|
:artist
is an expression that returns a Symbol
object corresponding to
artist
. You can think of :artist
as meaning the name
of the variable artist
, while plain artist
is the
value of the variable. In this example, we named the accessor
methods name
, artist
, and duration
. The
corresponding instance variables, @name
, @artist
, and
@duration
, will be created automatically. These accessor methods
are identical to the ones we wrote by hand earlier.
Song
object.
In languages such as C++ and Java, you'd do this with setter
functions.
class JavaSong { // Java code private Duration myDuration; public void setDuration(Duration newDuration) { myDuration = newDuration; } } s = new Song(....) s.setDuration(length) |
aSong.name
. So, it seems natural to be able to assign to these
variables when you want to set the value of an attribute. In keeping
with the Principle of Least Surprise, that's just what you do in Ruby.
class Song
|
||
def duration=(newDuration)
|
||
@duration = newDuration
|
||
end
|
||
end
|
||
aSong = Song.new("Bicylops", "Fleck", 260)
|
||
aSong.duration
|
» |
260
|
aSong.duration = 257 # set attribute with updated value
|
||
aSong.duration
|
» |
257
|
aSong.duration = 257
'' invokes the method
duration=
in the aSong
object, passing it 257
as
an argument. In fact, defining a method name ending in an equals sign
makes that name eligible to appear on the left-hand side of an
assignment.
Again, Ruby provides a shortcut for creating these simple attribute
setting methods.
class Song attr_writer :duration end aSong = Song.new("Bicylops", "Fleck", 260) aSong.duration = 257 |
class Song
|
||
def durationInMinutes
|
||
@duration/60.0 # force floating point
|
||
end
|
||
def durationInMinutes=(value)
|
||
@duration = (value*60).to_i
|
||
end
|
||
end
|
||
aSong = Song.new("Bicylops", "Fleck", 260)
|
||
aSong.durationInMinutes
|
» |
4.333333333
|
aSong.durationInMinutes = 4.2
|
||
aSong.duration
|
» |
252
|
durationInMinutes
seems to be an
attribute like any other. Internally, though, there is no
corresponding instance variable.
This is more than a curiosity. In his landmark book
Object-Oriented Software Construction ,
Bertrand Meyer
calls this the Uniform Access Principle.
By hiding the
difference between instance variables and calculated values, you are
shielding the rest of the world from the implementation of your class.
You're free to change how things work in the future without impacting
the millions of lines of code that use your class. This is a big win.
@@count
''.
Unlike global and instance variables, class variables must be
initialized before they are used. Often this initialization is just a
simple assignment in the body of the class definition.
For example, our jukebox may want to record how many times each
particular song has been played. This count would probably be an
instance variable of the Song
object. When a song is played, the
value in the instance is incremented. But say we also want to know
how many songs have been played in total. We could do this by
searching for all the Song
objects and adding up their counts, or
we could risk excommunication from the Church of Good Design and use a
global variable. Instead, we'll use a class variable.
class Song @@plays = 0 def initialize(name, artist, duration) @name = name @artist = artist @duration = duration @plays = 0 end def play @plays += 1 @@plays += 1 "This song: #@plays plays. Total #@@plays plays." end end |
Song#play
to return a
string containing the number of times this song has been played, along
with the total number of plays for all songs. We can test this easily.
s1 = Song.new("Song1", "Artist1", 234) # test songs..
|
||
s2 = Song.new("Song2", "Artist2", 345)
|
||
s1.play
|
» |
"This song: 1 plays. Total 1 plays."
|
s2.play
|
» |
"This song: 1 plays. Total 2 plays."
|
s1.play
|
» |
"This song: 2 plays. Total 3 plays."
|
s1.play
|
» |
"This song: 3 plays. Total 4 plays."
|
new
method creates a new Song
object but is not
itself associated with a particular song.
aSong = Song.new(....) |
File
represent open files
in the underlying file system. However, class File
also provides
several class methods for manipulating files that aren't open and
therefore don't have a File
object. If you want to delete a file,
you call the class method
File.delete
, passing in the name.
File.delete("doomedFile") |
class Example def instMeth # instance method end def Example.classMeth # class method end end |
SongList
that checked to see if a particular song
exceeded the limit. We'll set this limit using a class constant, which
is simply a constant (remember constants? they start with an uppercase
letter) that is initialized in the class body.
class SongList
|
||
MaxTime = 5*60 # 5 minutes
|
||
|
||
def SongList.isTooLong(aSong)
|
||
return aSong.duration > MaxTime
|
||
end
|
||
end
|
||
song1 = Song.new("Bicylops", "Fleck", 260)
|
||
SongList.isTooLong(song1)
|
» |
false
|
song2 = Song.new("The Calling", "Santana", 468)
|
||
SongList.isTooLong(song2)
|
» |
true
|
Logger.create
,
and we'll ensure that only one logging object is ever created.
class Logger private_class_method :new @@logger = nil def Logger.create @@logger = new unless @@logger @@logger end end |
Logger
's method new
private, we prevent anyone from
creating a logging object using the conventional constructor. Instead, we provide a class method,
Logger.create
. This uses the class variable @@logger
to
keep a reference to a single instance of the logger, returning that
instance every time it is called.[The implementation of
singletons that we present here is not thread-safe; if multiple
threads were running, it would be possible to create multiple logger
objects. Rather than add thread safety ourselves, however, we'd
probably use the Singleton
mixin supplied with Ruby, which is
documented on page 468.] We can check this by looking
at the object identifiers the method returns.
Logger.create.id
|
» |
537766930
|
Logger.create.id
|
» |
537766930
|
Shape
that represents a regular polygon. Instances of Shape
are created by giving the constructor the required number of sides and
the total perimeter.
class Shape def initialize(numSides, perimeter) # ... end end |
Shape
.
class Shape def Shape.triangle(sideLength) Shape.new(3, sideLength*3) end def Shape.square(sideLength) Shape.new(4, sideLength*4) end end |
initialize
, which is always private).
public
,
protected
, and private
. Each function can be used in two
different ways.
If used with no arguments, the three functions set the default access
control of subsequently defined methods. This is probably familiar
behavior if you're a C++ or Java programmer, where you'd use keywords
such as public
to achieve the same effect.
class MyClass def method1 # default is 'public' #... end protected # subsequent methods will be 'protected' def method2 # will be 'protected' #... end private # subsequent methods will be 'private' def method3 # will be 'private' #... end public # subsequent methods will be 'public' def method4 # and this will be 'public' #... end end |
class MyClass def method1 end # ... and so on public :method1, :method4 protected :method2 private :method3 end |
initialize
method is automatically declared
to be private.
It's time for some examples. Perhaps we're modeling an accounting
system where every debit has a corresponding credit. Because we want
to ensure that no one can break this rule, we'll make the methods that
do the debits and credits private, and we'll define our external
interface in terms of transactions.
class Accounts private def debit(account, amount) account.balance -= amount end def credit(account, amount) account.balance += amount end public #... def transferToSavings(amount) debit(@checking, amount) credit(@savings, amount) end #... end |
Account
objects to compare their raw
balances, but may want to hide those balances from the rest of the
world (perhaps because we present them in a different form).
class Account attr_reader :balance # accessor method 'balance' protected :balance # and make it protected def greaterBalanceThan(other) return @balance > other.balance end end |
balance
is protected, it's available
only within Account
objects.
Figure not available... |
person = "Tim"
|
||
person.id
|
» |
537771100
|
person.type
|
» |
String
|
person
|
» |
"Tim"
|
String
object with the
value ``Tim.'' A reference to this object is placed in the local
variable person
.
A quick check shows that the variable has indeed taken on the
personality of a string, with an object id, a type, and a value.
So, is a variable an object?
In Ruby, the answer is ``no.'' A variable is simply a reference to an
object. Objects float around in a big pool somewhere (the heap, most
of the time) and are pointed to by variables.
Let's make the example slightly more complicated.
person1 = "Tim"
|
||
person2 = person1
|
||
|
||
person1[0] = 'J'
|
||
|
||
person1
|
» |
"Jim"
|
person2
|
» |
"Jim"
|
person1
, but both person1
and person2
changed from ``Tim'' to ``Jim.''
It all comes back to the fact that variables hold references to
objects, not the objects themselves. The assignment of person1
to person2
doesn't create any new objects; it simply copies
person1
's object reference to person2
, so that both
person1
and person2
refer to the same object. We show
this in Figure 3.1 on page 31.
Assignment aliases objects, potentially giving you multiple
variables that reference the same object.
But can't this cause problems in your code? It can, but not
as often as you'd think (objects in Java, for example, work exactly
the same way). For instance, in the example in Figure
3.1, you could avoid aliasing by using the dup
method of String
, which creates a new String
object with identical
contents.
person1 = "Tim"
|
||
person2 = person1.dup
|
||
person1[0] = "J"
|
||
person1
|
» |
"Jim"
|
person2
|
» |
"Tim"
|
TypeError
exception.
person1 = "Tim" person2 = person1 person1.freeze # prevent modifications to the object person2[0] = "J" |
prog.rb:4:in `=': can't modify frozen string (TypeError) from prog.rb:4 |
Previous < |
Contents ^
|
Next >
|