Defining Classes and Creating Objects
9.1 Introduction
The preceding chapter introduced classes and objects and some of the vocabulary associated with objects. It also delved into a few of the built-in objects within Ruby. Now it is time to explore the true power of objects: the ability to create your own.
9.2 Instantiating Objects from Classes
As we mentioned in previous chapters, a class is a description of objects. A particular object is an instantiation of the class, having a unique name selected for it by the programmers. As with all information in a computer system, the various classes are stored in files. When reading the previous chapters, you probably noticed the following syntax in some examples:
customer
=
String
.
new
This is essentially the syntax for instantiating a new object. The
example shows that you are instantiating an object whose class is String
. The object’s name is customer
, and the
name is used as a variable from the class String
. Now you can manipulate the customer
variable using the different methods learned in the preceding
chapters.
Ruby provides many built-in classes like strings and arrays, but you can also create your own class. User-defined classes are a great way to group and categorize something’s characteristics. For example, if you want to organize a database for bank accounts, you can create a class describing the properties and behaviors of each bank account.
Example 9-1 provides an outline for defining your own class.
Class definition syntax
1
class
Classname
2
def
initialize
(
var1
,
var2
,
.
.
.
,
varn
)
3
@variable_1
=
var1
4
@variable_2
=
var2
5
.
.
.
6
@variable_n
=
varn
7
end
8
9
def
method_1
10
# code
11
end
12
13
def
method_2
14
# code
15
end
16
end
Gem of Wisdom
In Ruby, class names must begin with an uppercase letter.
We now describe lines 1–7 (the rest will be covered in the following
sections), which define a class and initialize variables local to the
class. To define a class, use the class
keyword followed by a descriptive name that characterizes an object. For
example, if you wanted to create a class for a bank account called
Account
, define the class by
typing:
class
Account
end
Another important keyword you should notice in the class definition
is def
(short for define). This keyword
is used to define new activities (called methods)
that may be performed on the object. This keyword is also used to define
the special method initialize
, which is called every time
a new instance of the class, that is, a new object, is created. All
classes have this special method, which is called a
constructor. We will explain class generation using
an example of a class describing bank accounts called Account
, the first iteration of which can be
seen in Example 9-2.
Account version 1
1
class
Account
2
def
initialize
(
balance
)
3
@balance
=
balance
4
end
5
end
The variables inside the parentheses after initialize
are the parameters that are assigned
when creating, or instantiating, the object.
Now when instantiating an object using the Account
class, the object will have a variable
called balance
with an initial value
that you can assign using a parameter. The special character (@
) is used to indicate that this is an
instance variable, meaning that it is a parameter of
the object. Variables might be specific to a single method in the class,
but these instance variables can be accessed by any method in the object
description. These instance variables are sometimes referred to
as storing the properties of an object.
We have just created a user-defined class, but how do you use it?
You can instantiate an object of the Account
class the same way you create new
strings and arrays:
bob
=
Account
.
new
(
10
.
00
)
Gem of Wisdom
Each created instance of a class will have its own unique instance variables. If a variable is prefixed with @@, it becomes a class variable that is shared across all instances of the class. We do not discuss class variables in this book.
This example creates an object called bob
of the Account
class. Remember when you created the
initialize
method? You assigned one
parameter called balance
; this is the
value in the parentheses. The parameter passed in the parentheses will
become the initial balance of Bob’s account.
What other variables should you consider adding to the Account
class? What kind of behaviors should the
class contain? In the following sections, we describe grouping data, as
well as adding your own methods and working with the data through the
object’s methods.
9.3 Data and Methods
Objects are made up of two important concepts: the data the object holds (the instance variables) and the actions the object can perform (the methods).
9.3.1 Grouping Data and Methods
The previous section detailed how to create and instantiate an
object of an Account
class, and while
reading it you might have been thinking to yourself, “How was I supposed
to know that an Account
class needs a
balance
variable?” The answer is that
no class requires any particular piece of data, but classes are used to
group related pieces of data together, and it only makes sense that an
account has a balance. Likewise, there are other things that would come
as a part of a bank account. Depending on the nature of the bank
account, the type of data included would change. For example, a savings
account wouldn’t necessarily include the same data as a checking
account. Regardless, we are talking about a generic bank account, and
additional data possibly included are name, phone number, Social
Security number, minimum balance, and maximum balance. We now introduce
two additional instance variables to our Account
class, as shown in Example 9-3.
Account version 2
1
class
Account
2
def
initialize
(
balance
,
name
,
phone_number
)
3
@balance
=
balance
4
@name
=
name
5
@phone_number
=
phone_number
6
end
7
end
Gem of Wisdom
Note that the instance variables balance
, name
, and phone_number
are assigned in the order the
parameters were passed to the initialize
method; however, this is not
required. It is done merely for convenience to the reader. Also, the
actual names can be chosen arbitrarily. For example, @cash = balance
, while allowed, is
discouraged.
Our bank account is beginning to make a bit more sense. On top of
just having a balance, there is a name and a phone number attached to
the account, so we can uniquely determine whose account it is. Now that
our Account
class constructor has
changed, let’s see how to initialize Bob’s bank account when he has $10
as his starting balance, and has a phone number of 716-634-9483.
bob
=
Account
.
new
(
10
.
00
,
"Bob"
,
7166349483
)
An object also contains methods. Just like data, methods are
logically grouped together based on the class. The purpose of a method
is to accomplish a task, so we must ask ourselves, what actions should a
bank account have? We would expect that at the very least we could
withdraw from and deposit to our account. Let’s add these methods to our
Account
class, as shown in Example 9-4.
Account version 3
1
class
Account
2
def
initialize
(
balance
,
name
,
phone_number
)
3
@balance
=
balance
4
@name
=
name
5
@phone_number
=
phone_number
6
end
7
8
def
deposit
(
amount
)
9
# code
10
end
11
12
def
withdraw
(
amount
)
13
# code
14
end
15
end
Aside from the missing implementation code on lines 9 and 13, our
Account
class implementation is
looking pretty good. Not only can Bob open an account, but he can also
deposit or withdraw money when he desires.
The Account
class is almost
finished, and the only thing left to do before Bob is able to open a
bank account is to implement the deposit and withdraw methods.
9.3.2 Implementing Methods
A key advantage of objects is that they abstract the details of
their operations away from the code that uses them. Once the details of
the Account
class are finalized, a
programmer can use the class without knowing any of those details. The
programmer need only know what data are required to initialize the
class, and what data are required for each method in the class. For
example, consider the String
class
provided in the Ruby standard library. When we use the capitalize
method, we do not know how String
stores the data, nor how the data get
accessed. All we need to know is that the capitalize
method capitalizes the first letter
of the string.
As we implement an object, we must consider every detail of its
operation. The deposit
method, for example, must add
the value of the parameter passed to the previous @balance
and store the result back in @balance
. Let’s take a look at the
implementation of the deposit and withdraw methods, as shown in Example 9-5.
Account version 4
1
class
Account
2
def
initialize
(
balance
,
name
,
phone_number
)
3
@balance
=
balance
4
@name
=
name
5
@phone_number
=
phone_number
6
end
7
8
def
deposit
(
amount
)
9
@balance
+=
amount
10
end
11
12
def
withdraw
(
amount
)
13
@balance
-=
amount
14
end
15
end
Gem of Wisdom
Recalling our earlier Gem of Wisdom and looking at Example 9-5, using our shorthand construct
known as op=
, in line 9, the
variable @balance
is incremented by
the value of amount
, meaning
@balance = @balance + amount
, and
in line 13, the meaning of the statement is @balance = @balance - amount
.
To use these newly defined methods, we must initialize the classes
and then access them as we did with built-in methods. Note in the
following code that this is the first time we import definitions using
the require
command. For example, to
create an account for Mary, with $500, and then to deposit another $200,
we would perform the following steps in irb
:
irb
(
main
):
003
:
0
>
require
'account_4.rb'
=>
true
irb
(
main
):
004
:
0
>
mary_account
=
Account
.
new
(
500
,
"Mary"
,
8181000000
)
=>
#<Account:0x3dfa68 @balance=500, @name="Mary", @phone_number=8181000000>
irb
(
main
):
005
:
0
>
mary_account
.
deposit
(
200
)
=>
700
irb
(
main
):
006
:
0
>
mary_account
=>
#<Account:0x3dfa68 @balance=700, @name="Mary", @phone_number=8181000000>
As can be seen from the output, Mary’s account now holds 700 in
its @balance
variable. However, it
would be much nicer to provide a helper method to display this
information. The display
method is an
often-used method for outputting the contents of an object’s instance.
For the Account
class, we can output
the name, phone number, and account balance to the screen with the code
shown in Example 9-6.
Display method
1
def
display
()
2
puts
"Name: "
+
@name
3
puts
"Phone Number: "
+
@phone_number
.
to_s
4
puts
"Balance: "
+
@balance
.
to_s
5
end
Now we can immediately see the result of our actions. For example, try running the following code, which indirectly transfers $200 from Bob’s account to Mary’s:
bob_account
=
Account
.
new
(
500
,
"Bob"
,
8181000000
)
mary_account
=
Account
.
new
(
500
,
"Mary"
,
8881234567
)
bob_account
.
withdraw
(
200
)
mary_account
.
deposit
(
200
)
bob_account
.
display
()
mary_account
.
display
()
Note that in both the method definition in Example 9-6 and in its use in the preceding code, empty parentheses are included. Such use is optional; however, we include it to reinforce the fact that parameters are needed.
At the end of executing those instructions, bob_account
would have $300 as its balance,
and mary_account
would have $700.
However, every time we would want to use the Account
class to transfer money, we would have
to write two lines: one for withdrawing from the old account and another
for depositing to a new one. It would be much easier to use the Account
class if the two functionalities were
combined into a single method. This single method would need to affect
two separate instances of a single class. This is done by passing an
account object to a new method called transfer
, shown in Example 9-7.
Transfer method
1
def
transfer
(
amount
,
target_account
)
2
@balance
-=
amount
3
target_account
.
deposit
(
amount
)
4
end
Finally, all our methods thus far affected values stored in the program.
However, none of our defined methods returned a value to the
invoking statement. That is, if one wished to assign the balance of an
account to a variable, this balance would need to be returned after a
sequence of deposits and withdrawals. To obtain this value, a method
must be defined that returns a value. We define such a method, called
status
,
as shown in Example 9-8.
Status method
1
def
status
2
return
@balance
3
end
Two items are critical to note about the definition of the
status
method. First, the return
construct returns the value of @balance
to the method-invoking element. For
the sophisticated Ruby programmer, the reality is that Ruby always
returns the value of the last statement executed. However, if a
different value or better clarity is desired, a return
statement is often used.
Second, since there is no local overriding parameter called
@balance
, the global value for
@balance
is accessed. Example 9-9 contains the full
implementation of our Account
class.
Account—final version (version 5)
1
class
Account
2
def
initialize
(
balance
,
name
,
phone_number
)
3
@balance
=
balance
4
@name
=
name
5
@phone_number
=
phone_number
6
end
7
8
def
deposit
(
amount
)
9
@balance
+=
amount
10
end
11
12
def
withdraw
(
amount
)
13
@balance
-=
amount
14
end
15
16
def
display
17
puts
"Name: "
+
@name
18
puts
"Phone number: "
+
@phone_number
.
to_s
19
puts
"Balance: "
+
@balance
.
to_s
20
end
21
22
def
transfer
(
amount
,
target_account
)
23
@balance
-=
amount
24
target_account
.
deposit
(
amount
)
25
end
26
27
def
status
28
return
@balance
29
end
30
end
9.4 Summary
We described how to create objects and methods. The special method
initialize
was discussed as the means
to implement a constructor. Also, class variables were described.
9.4.1 Key Concepts
Objects are created by instantiation. This is done via the constructor contained in the class definition.
When creating a class, it is important to keep in mind that objects from the class are meant to group data and methods together.
A key point to keep in mind when working with objects is that once an object has been created, it abstracts the details away from the program that uses it. In other words, you can use an object without seeing the details of that object directly.
9.4.2 Key Definitions
Instantiating objects from classes: The creation of new objects.
Constructor: A special method that all classes have that initializes the data in an object each time a new object is created.
Instance variable: A variable that is unique to an instance of a class. It stores information relevant to the object.
9.5 Exercises
Create two classes to represent the following two objects: televisions and speakers. Include an
initialize
function and several methods to interact with your objects.Given two Cartesian points (x1, y1) and (x2, y2), the slope of the line segment connecting them is given by the formula (y2 - y1)/(x2 - x1).
Write a class that represents a Cartesian point. Define a method
find_slope
that takes in a Cartesian point object and finds the slope between the two points. Test your class with the following:(0,0) (3,4)
(2,3) (6,5)
(2,2) (2,7)
What happens in the last case? Why does that happen?
Define a class that compares two numbers and outputs the larger one. Test your solution.
Briefly explain the code illustrated in Example 9-10.
Code for Exercise 4
1
class
Profile
2
def
initialize
(
name
,
phone_number
)
3
@name
=
name
4
@phone_number
=
phone_number
5
end
6
7
def
display
8
puts
"Name ==> "
+
@name
9
puts
"Phone number ==> "
+
@phone_number
.
to_s
10
end
11
end
Write a
Student
class that contains a student’s name, gender, phone number, and exam score. It should also include theinitialize
,accumulated_score
, anddisplay
methods.