Defining Classes and Creating Objects - Computer Science Programming Basics in Ruby (2013)

Computer Science Programming Basics in Ruby (2013)

Chapter 9. Defining Classes and Creating Objects

IN THIS CHAPTER

§ Instantiating objects from classes

§ Data and methods in 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.

Example 9-1. 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.

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 aninstance 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 balancevariable?” 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.

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.

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.

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.

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.

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.

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.

Example 9-9. 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

1. Create two classes to represent the following two objects: televisions and speakers. Include an initialize function and several methods to interact with your objects.

2. 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:

a. (0,0) (3,4)

b. (2,3) (6,5)

c. (2,2) (2,7)

What happens in the last case? Why does that happen?

3. Define a class that compares two numbers and outputs the larger one. Test your solution.

4. Briefly explain the code illustrated in Example 9-10.

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

5. Write a Student class that contains a student’s name, gender, phone number, and exam score. It should also include the initialize, accumulated_score, and display methods.