Object Inheritance - Computer Science Programming Basics in Ruby (2013)

Computer Science Programming Basics in Ruby (2013)

Chapter 10. Object Inheritance

IN THIS CHAPTER

§ Inheritance

§ Overriding methods

§ Accessing the superclass

§ Applications of inheritance

10.1 Introduction

The preceding two chapters introduced you to the use and creation of simple objects. Now it is time to show how objects can be created from other objects. This chapter covers inheritance, method overriding, and various applications of objects.

10.2 Inheritance

At this point in the book, your programming style is starting to become more sophisticated. No longer are you writing code where one long file encompasses everything you do. Now you are using objects, and you are also beginning to program in an object-oriented manner. Object-oriented programming (OOP) is a powerful form of programming that is currently extremely popular and the backbone for many new languages, including Ruby.

GEM OF WISDOM

Object inheritance is one of the most powerful parts of object-oriented programming. Inheritance enables hierarchical decomposition of structures into logically encapsulated units.

Thus far, we have talked about creating simple objects that are independent of one another. However, one of the most powerful abilities OOP has is the ability to define relationships between objects. The first of these relationships is known as inheritance. To understand the concept of inheritance, imagine a ball (any spherical object that could pass for a ball will do). Perhaps you are thinking of a baseball, a tennis ball, or a ping-pong ball; it does not matter which because all those balls have a large degree of similarity among them, despite being different types of balls or even different objects. If we needed to accomplish some task and asked you for a ball, they could all work despite their differences. Inheritance allows us to define these types of relationships with Ruby objects. This ends up saving the programmer significant time and code because she or he need not redefine parts of the objects that are similar.

Let’s return to our bank account example from the preceding chapter. It defined what is essentially a checking account, and our bank is no longer happy with just this one type of account; now it wants to expand to include savings accounts. The first thing to notice are the similarities between a savings account and a checking account: they both maintain a balance and can have money withdrawn from them and deposited to them. The class that defines the similarities in the relationship is referred to as the parent class, or the superclass.

The main differences between the two bank accounts are that you cannot withdraw beyond the minimum balance from a savings account (we will touch on this in the next section) and that a savings account generates interest. The class that defines the differences in the relationship is referred to as the child class or the subclass. Now we will use inheritance to define this SavingsAccount class (see Example 10-1).

Example 10-1. SavingsAccount version 1

1 require_relative '../chapter_09/account_5.rb'

2

3 class SavingsAccount < Account

4 def initialize(balance, name, phone_number, interest, minimum)

5 super(balance, name, phone_number)

6 @interest = interest

7 @minimum = minimum

8 end

9

10 def accumulate_interest

11 @balance += @balance * @interest

12 end

13 end

Note that we use the require_relative command instead of the require command. require loads files that are installed as Ruby libraries or files for which the full path to the file is given. require_relative is used to load files without specifying the full path to the file; it looks for files in a location relative to the file require_relative is used in.

The first thing to note in the code provided in Example 10-1 is the < symbol (on line 3). This is the symbol used to define inheritance in Ruby. In this case, the parent class is the Account class, which is predefined via line 1, and the child class is the SavingsAccount class. See the account class in Example 9-9.

The next thing to look over is the constructor of the SavingsAccount class, expressed by the initialize method on line 4. Immediately, in line 5, this method calls a method named super(), which is the equivalent of calling the initialize method for the superclass Account. After this, we initialize the instance variables @interest and @minimum. It is important to note that these pieces of data distinguish a SavingsAccount from a CheckingAccount and the subclass from the superclass.

Finally, there is the accumulate_interest method, which is just a simple interest calculation.

However, thanks to inheritance, the SavingsAccount class can do more than just accumulate interest. It also inherits all the data and methods from the Account class. Table 10-1 is a summary of everything inherited by the SavingsAccount class.

Table 10-1. SavingsAccount inherited summary

Data

Methods

@balance

withdraw(amount)

@name

deposit(amount)

@phone_number

transfer(amount, targetAccount)

display

If we create an instance of the SavingsAccount class:

account = SavingsAccount.new(200.00, "Reynolds",

9694905555, 0.015, 150)

we can then call any of the following methods:

account.deposit(amount)

account.withdraw(amount)

account.transfer(amount, targetAccount)

account.accumulate_interest

account.display

This explicitly shows the power of inheritance. Although we never defined four of the five methods shown for the SavingsAccount class, we can use them because they are inherited from the parent class, Account. The SavingsAccount class consists of only 11 lines of code, but it has as much functionality as 41 lines of code (the number of lines of code in Example 10-1 plus Example 9-9).

If we were to return to the transfer(amount, targetAccount) method from the preceding chapter we would see that it was designed to transfer money from one account to another. At the time we created the method, we had not designed a SavingsAccount and were content with it working only on Account objects. However, it will work on SavingsAccount objects, because a SavingsAccount is an Account; it has enough similarity for a transfer between accounts to be possible. This is another powerful ability granted by inheritance, and it is known as polymorphism. Polymorphism, however, does not work both ways. With the transfer(amount, targetAccount) method example, the polymorphism is from the subclass to the superclass. Polymorphism will not work when you are trying to morph from a superclass to a subclass, because the subclass has abilities the superclass does not. To express this in an example, imagine trying to call the accumulate_interest() method from an Account object; it won’t work because only SavingsAccount objects, not Account objects, have the accumulate_interest() method.

10.3 Basic Method Overriding

When extending a class, it is sometimes convenient to alter methods that already exist in the class’s superclass. For example, both the checking and saving accounts need a method for withdrawing money. However, the methods are only the same on the outside. Unlike the regular checking account, the savings account needs to check if the balance would fall below the minimum allowed. To achieve this, the SavingsAccount class will need to override thewithdraw method by defining its own withdraw functionality, as shown in Example 10-2. Overriding is accomplished by using the same name in the local class. The local definition always supersedes the parent definition.

Example 10-2. SavingsAccount version 2

1 require_relative '../chapter_09/account_5.rb'

2

3 class SavingsAccount < Account

4 def initialize(balance, name, phone_number, interest, minimum)

5 super(balance, name, phone_number)

6 @interest = interest

7 @minimum = minimum

8 end

9

10 def accumulate_interest

11 @balance += @balance * @interest

12 end

13

14 def withdraw(amount)

15 if (@balance - amount >= @minimum)

16 @balance -= amount

17 else

18 puts "Balance cannot drop below: " + @minimum.to_s

19 end

20 end

21 end

Instead of calling on the withdraw method that belongs to Account, the SavingsAccount class will use the new withdraw method that overrode it. As a result, any instances of SavingsAccount will not be able to fall below their minimum account balances. This powerful property of OOP has its problems. It implies that the writer of a subclass be fully cognizant of the methods and instance variables of the superclass.

10.4 Accessing the Superclass

In many cases, the overriding methods will have similar functionality to the methods they override. It is counterproductive to the concept of inheritance to just rewrite the same methods again with slightly altered code. Inheritance exists to make code reuse as easy as possible. As such, it provides a way to avoid rewriting the superclass method. Simply insert the word super with all the parameters that would be used to call the superclass method bearing the same name wherever you would like the superclass’s method, just like the initialize method. Applying this to our new SavingsAccount class, we get the code in Example 10-3.

Example 10-3. SavingsAccount version 3

1 require_relative '../chapter_09/account_5.rb'

2

3 class SavingsAccount < Account

4 def initialize(balance, name, phone_number, interest, minimum)

5 super(balance, name, phone_number)

6 @interest = interest

7 @minimum = minimum

8 end

9

10 def accumulate_interest

11 @balance += @balance * @interest

12 end

13

14 def withdraw(amount)

15 if (@balance - amount >= @minimum)

16 super(amount)

17 else

18 puts "Balance cannot drop below: " + @minimum.to_s

19 end

20 end

21 end

In our example, obviously the benefits seem minimal. However, for complex programs, the advantages of using predefined classes are tremendous. Not only are we saving ourselves the time of rewriting the class, but we are also making code maintenance easier. If the withdraw method needs to be updated, we can update it in the Account class. Any subclasses that use it as their superclass will be updated accordingly, wherever they have called super.

10.5 Applications

Inheritance is a way to form new classes that borrow attributes and behaviors of previously defined classes. After learning about inheritance and method overriding, you are probably wondering when you will ever need to use them. What is the big deal about inheritance? Why can’t we just make a bunch of different classes? To put it simply, it saves significant unnecessary coding by eliminating code duplication, and it simplifies software testing and maintenance since functionality and local data are isolated. Here are several other examples for which you can use inheritance.

10.5.1 Person Database

A contractor is looking for a way to keep track of all the people in his organization. He has full-time and part-time employees, student interns, and volunteers. In this example, you can make a class called Person. This class can have the individual’s name, address, phone number, email address, and weekly hours worked. You can have a method called email that emails all the employees to remind them to turn in their time sheets. Then you can have subclasses called Full, Part, Intern, and Volunteer. For the Full subclass, you can include variables like hourly wage and overtime pay. For a behavior you can have a process_payment method to deposit money into the employee’s bank account. A student Intern would have different variables. Maybe you want to keep track of who has the highest grade-point average or test scores to see which one is the top intern. Thus, you would create some variables for the aforementioned categories. For members of the Volunteer subclass, you probably would not want to send them an email about turning in their time sheets to get paid, so you should make a custom method that asks them how many hours they volunteered. You can create an email method that overrides the previous one.

10.5.2 Grocery Store

You go to a small grocery store and overhear the owner complaining about keeping track of his food. He orders food every week, but his employees have no idea what to do with the food when it comes in. You can create a database program with a class called Food. This class can have variables like the name of the item, the price, and the location where it is stored. You can have a method called PrintLabel to create price labels to stick on the food. Possible subclasses to consider are Fruit, Meat, and Grain. Most of the foods will have traits in common, so you won’t need to worry about creating too many variables. The PrintLabel method will create tiny stickers you can put on the food, but what if you had a Fruit like raisins or grapes? There is no possible way you can print labels to stick on each individual fruit. You will probably want to use method overriding to print bigger labels you can stick on the shelf near the respective fruit.

10.5.3 Video Games

You have been hired to create a role-playing video game called “Pirates and Ninjas of the Pacific,” where players can choose to play as ninjas or pirates. How are you going to keep track of all the players’ characters? You can create a class called Player with variables for name, health points, and experience. You should also include methods for walking and fighting so that the player can move around and kill monsters by fighting with them. Since pirates and ninjas obviously have different skills, you will probably want to create subclasses called Pirate and Ninja. Ninjas generally fight using hand-to-hand combat; so you would not need to change their Fight method. Pirates, on the other hand, generally lack hand-to-hand combat skills and use guns instead. What’s the solution? Override their Fight method with something that allows them to shoot guns. But ninjas don’t always just fight with their hands, so you could create additional methods like throw and jump.

Looking at this and the preceding examples, you can get the general idea for when classes, inheritance, and method overriding can be useful. These techniques are used in every field of computer science. You can implement inheritance whether you are programming for a contractor, a grocer, or a video game company. As an exercise, try to come up with three of your own examples for which you can use classes, inheritance, and method overriding.

10.6 Summary

We have introduced the notion of object inheritance and shown some examples.

10.6.1 Key Concepts

§ One of the most powerful tools in object-oriented programming (OOP) is that objects can be created from other objects and use the resources of the parent object. This is known as inheritance. In this relationship, the parent class or superclass defines the relationship with the child class or subclass.

§ Subclasses inherit both data and methods from their parent class.

§ A key point to keep in mind when working with inheritance is that polymorphism often takes place, which in some cases can lead to a need for method overriding.

10.6.2 Key Definitions

§ Inheritance: When the relationship between two classes is defined.

§ Superclass: The class that defines the object for the relationship.

§ Subclass: The class for which the relationship is defined.

§ Method overriding: When the method for a parent class is redefined for a child class.

§ Polymorphism: Allows different data types to be handled by a unified interface.

10.7 Exercises

1. The class shown in Example 10-4 is used to keep track of the inventory for a company. It allows the user to assign a name and a manufacturer to each item. Write a class ItemNumber that inherits from Item that lets the user input a number of items owned for each item. Create an instance of the Item class and then an instance of the ItemNumber class.

Example 10-4. Code for Exercise 1

1 class Item

2 def initialize(item, maker)

3 @item = item

4 @maker = maker

5 end

6

7 def display

8 puts "Item ==> " + @item

9 puts "Maker ==> " + @maker

10 end

11 end

2. Explain polymorphism.

3. Define OOP.

4. A university has three kinds of students: full-time students, part-time students, and off-campus students. Each student has the following information: name, address, phone number, Social Security number, student ID, and GPA. The full-time students should take at least 12 credits per semester. The part-time students should take less than 12 credits per semester. The off-campus students have no limit on credits per semester. The tuition fee for full-time students is $8,500 for up to 18 credits per semester and $600 for every credit over 18 credits. The tuition fee for part-time students is $750 per credit. The tuition fee for off-campus students is $520 per credit. Write a program that lists student information and calculates their tuition fees.

5. Write a program for the grocery store example described in Section 10.5.2, “Grocery Store.”