7.1. What Is Polymorphism?

Polymorphism refers to the capability of two or more objects belonging to different classes to respond to exactly the same method call in different class-specific ways.

As an example, suppose that we instruct three different people—a surgeon, a hair stylist, and an actor—to "cut":

  • The surgeon would begin to make an incision.

  • The hair stylist would begin to cut someone's hair.

  • The actor would abruptly stop acting out the current scene, awaiting directorial guidance.

These three different professionals can be thought of as objects belonging to different professional classes. Each was given the same message—"cut"—but knew the specific details of what this message meant to him or her by virtue of knowing the profession (class) that he or she is associated with.

Turning to a software example relevant to the Student Registration System (SRS), assume that we' defined a Student base class and two derived classes named GraduateStudent and UndergraduateStudent.

In Chapter 5, we discussed the fact that a Print method intended to print the values of all of a Student's fields wouldn't necessarily suffice for printing the field values for a derived class such as GraduateStudent because the code as written for the Student class wouldn't know about any fields that may have been added to the derived class. The method call Print means something different to each Student-derived class, just as "cut" meant something different to the people in the previous example. We would therefore override the Print method of Student to create specialized versions of the method for all of its derived classes. The code for doing so, which was first introduced in Chapter 5, is repeated again here for you to review; we've added the UndergraduateStudent class code and have also made a few minor enhancements to the Print method for the other two classes:

// Student.cs

using System;

public class Student
{
  private string name;
  private string studentId;
  private string major;
  private double gpa;

  // Public properties also provided (details omitted)...

  public virtual void Print() {
					// We can only print the fields that the Student class
					// knows about.
					Console.Write("Student Name:  " + Name);
					Console.Write("  Student ID:  " + StudentId);
					Console.Write("  Major Field: " + Major);

Console.WriteLine("  GPA:     " + Gpa
					}
}


// GraduateStudent.cs

using System;

public class GraduateStudent : Student
{
  // Adding several fields.
  private string undergraduateDegree;
  private String undergraduateInstitution;

  // Public properties also provided (details omitted)...

  // Overriding the Print method.
					public override void Print() {
					// Reuse code by performing the Print method of the
					// Student base class...
					base.Print();
					//...and then go on to print this derived class's specific fields.
					Console.Write("Undergrad. Deg.:  " +  UndergraduateDegree);
					Console.Write("  Undergrad. Inst: " + UndergraduateInstitution);
					Console.WriteLine("  THIS IS A GRADUATE STUDENT");
  }
}


// UndergraduateStudent.cs

using System;

public class UndergraduateStudent : Student
{
  // Adding a field.
  private string highSchool;

  // Public property also provided (details omitted)...

  // Overriding the Print method.
					public override void Print() {
					// Reuse code from the Student base class...
					base.Print();

//...and then go on to print this derived class's specific fields.
					Console.Write("High School Attended:  " + HighSchool);
					Console.WriteLine("THIS IS AN UNDERGRADUATE STUDENT...");
  }
}

In our main SRS application, we might declare an array called studentBody designed to hold references to Student objects. We then populate the array with Student object references—some graduate students and some undergraduate students, randomly mixed—as shown here:

// Declare and instantiate an array.
Student[] studentBody = new Student[4];

// Instantiate various types of Student object.
UndergraduateStudent u1 = new UndergraduateStudent();
UndergraduateStudent u2 = new UndergraduateStudent();
GraduateStudent g1 = new GraduateStudent();
GraduateStudent g2 = new GraduateStudent();
// etc.

// "Stuff" them into the array in random order.
studentBody[0] = u1;
studentBody[1] = g1;
studentBody[2] = g2;
studentBody[3] = u2;
// etc.

Because we're storing both GraduateStudent and UndergraduateStudent objects in this array, we declared the array to be of a base type common to all objects that the array is intended to contain, namely, Student. By virtue of the "is a" nature of inheritance, an UndergraduateStudent object is a Student, and a GraduateStudent object is a Student, so the compiler won't complain when we insert either type of object into the array.

NOTE

The compiler would object, however, if we tried to insert a Professor object into the same array, because a Professor isn't a Student, at least not in terms of the class hierarchy that we've defined for the SRS. If we wanted to include Professors in our array along with various types of Students, we'd have to declare the array as holding a base type common to both the Student and Professor classes, namely, Person.

Perhaps we'd like to print the field values of all of the students in our studentBody array. We'd want each Student object—whether it is a graduate student or an undergraduate student—to use the version of the Print method appropriate for its class. The following code will accomplish this nicely:

// Step through the array (collection)...

for (int i = 0; i < studentBody.Length i++) {
  //...invoking the Print method of the ith student object.
					studentBody[i].Print();
					Console.WriteLine();
}

As we step through this collection of Student objects (assuming that their fields were assigned values), processing them one by one, each object will automatically know which version of the Print method it should execute, based on its own internal knowledge of its type/class (GraduateStudent versus UndergraduateStudent in this example). We'd wind up with a report similar to the following, in which the highlighted lines emphasize the differences in output between the GraduateStudent and UndergraduateStudent versions of the Print method:

Student Name:  John Smith
Student No.:  12345
Major Field:  Biology
GPA:  2.7
High School Attended:  Rocky Mountain High
					THIS IS AN UNDERGRADUATE STUDENT...

Student Name:  Paula Prabhu
Student No.:  34567
Major Field:  Education
GPA:  3.6
Undergrad. Deg.:  B.S. English
					Undergrad. Inst.:  UCLA
					THIS IS A GRADUATE STUDENT...

Student Name:  Romeo Cardiz
Student No.:  98765
Major Field:  Computer Science
GPA:  4.0
Undergrad. Deg.:  B.S. Computer Engineering
					Undergrad. Inst.:  Case Western Reserve University
					THIS IS A GRADUATE STUDENT...

Student Name:  James Roberts
Student No.:  82640
Major Field:  Math
GPA:  3.1
High School Attended:  James Ford Rhodes High
					THIS IS AN UNDERGRADUATE STUDENT...

The term polymorphism is defined in Merriam-Webster's dictionary as

"The quality or state of being able to assume different forms."

The following line of code is said to be polymorphic because the code performed in response to the method call can take many different forms, depending on the class identity of the object:

studentBody[i].Print();

Of course, this approach of iterating through a collection to ask objects one by one to each do something in its own class-specific way won't work unless all objects in the collection understand the message being sent. That is, all objects in the studentBody array must have defined a method with the signature: Print(). However, we've guaranteed that every object in the studentBody array will have such a method:

  • First of all, we declared the array to hold objects of type Student (or derived classes thereof).

  • Secondly, we provided the Student base class with a parameterless Print method. Had we not done so, the compiler would have objected to the following line of code because it would have checked the Student class for the presence of a Print method:

    studentBody[i].Print();

  • Then, by virtue of inheritance, any derived class of Student is guaranteed to either inherit the Student version of the Print method or to optionally override it with one of its own. As you learned in Chapter 5, there is no way for a derived class to "uninherit" a method defined for any of its ancestor classes. The bottom line is that all objects declared to be of type Student are guaranteed to be Print savvy!

Reflecting for a moment, you can now see that you've previously learned everything that you need to know about C# objects to facilitate polymorphism—namely, inheritance plus overriding—before this discussion of polymorphism even began. Inheritance combined with overriding makes polymorphism possible.

7.1.1. Polymorphism Simplifies Code Maintenance

To appreciate the power of polymorphism, let's look at how we might have to approach this same challenge—handling different objects in different type-specific ways—with a programming language that doesn't support polymorphism.

In the absence of polymorphism, we'd typically handle scenarios having to do with a variety of different kinds of students using a series of if tests:

for (int i = 0; i < studentBody.Length; i++) {
  // Process the ith student.
						// Pseudocode.
						if (studentBody[i] is an undergraduate studentr)
						studentBody[i].PrintAsUndergraduateStudent();
						else if (studentBody[i] is a graduate student)
						studentBody[i].PrintAsGraduateStudent();
						else if...
}

As the number of cases grows, so too does the "spaghetti" nature of the resultant code! And, keep in mind that this sort of if test can occur in countless places throughout an application. Maintenance of such code quickly becomes a nightmare.

Let's now contrast this with our polymorphic iteration through the studentBody array:

// Step through the array (collection)...
for (int i = 0; i < studentBody.Length; i++) {
  //...invoking the Print method of the ith student object.
						studentBody[i].Print();
}

Because client code can be written to operate on a variety of objects without knowing specifically what type of object is involved, such client code is robust to change. For example, let's say that long after our SRS application has been coded and tested, we derive classes called MastersStudent and PhDStudent from GraduateStudent, each of which in turn overrides the Print method to provide its own "flavor" of printing, as shown in Figure 7-1.

Figure 7.1. Subsequent overriding of Print by newly derived classes

We are now free to randomly insert MastersStudent and PhDStudent objects into the mix of GraduateStudents and UndergraduateStudents in the array, and our polymorphic array iteration code doesn't have to change!

// Declare and instantiate an array.
Student[] studentBody = new Student[4];

// Instantiate various types of Student object. We're now dealing with four
						// different derived types.
UndergraduateStudent u1 = new UndergraduateStudent();
PhDStudent p1 = new PhDStudent();
GraduateStudent g1 = new GraduateStudent();

MastersStudent m1 = new MastersStudent();
// etc.

// Insert them into the array in random order.
studentBody[0] = u1;
studentBody[1] = p1;
studentBody[2] = g1;
studentBody[3] = m1;
// etc.

// Then, later in our application...

// This is the exact same code that we've seen before!
// Step through the array (collection)...
for (int i = 0; i < studentBody.Length; i++) {
  //...and invoke the Print method of the ith student object.
  // Because of the polymorphic nature of C#, this next line didn't
						// require any changes!
						studentBody[i].Print();
}

This is because the newly derived types (MastersStudent and PhDStudent) are, as extensions of Student, once again guaranteed to understand the same Print method call by virtue of inheritance plus optional overriding.

The story is quite different, however, with the nonpolymorphic example we crafted earlier. That version of client code would indeed have to change to accommodate these new student types; specifically, we'd have to hunt through our application to find every situation in which we're trying to sort out types of Student and complicate our if tests even further by adding additional cases as shown here, causing the "spaghetti piles" to grow ever taller:

for (int i = 0; i < studentBody.Length; i++) {
  // Process the ith student.
  // Pseudocode.
  if (studentBody[i] is an undergraduate student )
    studentBody[i].PrintAsUndergraduateStudent();
  else if (studentBody[i] is a masters student) 
						studentBody[i].PrintAsMastersStudent(); 
						else if (studentBody[i] is a PhD student) 
						studentBody[i].PrintAsPhDStudent(); 
  else if (studentBody[i] is a general graduate student )
    studentBody[i].PrintAsGraduateStudent();
  else if...
}

As we saw with encapsulation and information hiding earlier, polymorphism is another extremely powerful feature of OOPLs that can simplify method calls and minimize "ripple effects" on existing applications when requirements inevitably change after an application has been deployed.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset