Team LiB
Previous Section Next Section

What Is Polymorphism?

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

As an example, if we were to instruct three different people—a surgeon, a hair stylist, and an actor—to "cut!", then

These three different professionals may 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 SRS, assume that we've defined a Student base class and two derived classes, 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 attributes wouldn't necessarily suffice for printing the attribute values for a derived class such as GraduateStudent, because the code as written for the Student class wouldn't know about any attributes that may have been added to the derived class. 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 attributes that the Student class
    // knows about.
    Console.WriteLine("Student Name: " + Name + "\n" +
      "Student No.: " + StudentId + "\n" +
      "Major Field: " + Major + "\n" +
      "GPA: " + Gpa);
  }
}

// GraduateStudent.cs

using System;

public class GraduateStudent : Student
{
  // Adding several attributes.
  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 attributes.
    Console.WriteLine("Undergrad. Deg.: " + UndergraduateDegree +
      "\n" + "Undergrad. Inst.: " +
      UndergraduateInstitution + "\n" +
      "THIS IS A GRADUATE STUDENT ...");
  }
}

// UndergraduateStudent.cs

using System;

public class UndergraduateStudent : Student
{
  // Adding an attribute.
  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 attributes.
    Console.WriteLine("High School Attended: " + HighSchool +
      "\n" + "THIS IS AN UNDERGRADUATE STUDENT ...");
  }
}

In our main SRS application, we 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[20];

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

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

Since we're storing both GraduateStudent and UndergraduateStudent objects in this array, we've 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, and so the compiler won't complain when we insert either type of object into the array.

Note?/td>

Note that 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 attribute 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 < 20; i++) {
    // ... invoking the Print method of the ith student object.
    studentBody[i].Print();
  }

As we step through this collection of Student objects, 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 vs. UndergraduateStudent, in this example). We'd wind up with a report similar to the following, where 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 Green
Student No.:  34567
Major Field:  Education
GPA: 3.6
Undergrad. Deg.: B.S. English
Undergrad. Inst.: UCLA
THIS IS A GRADUATE STUDENT ...

Student Name:  Dinesh Prabhu
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

Note?/td>

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

The line of code

  studentBody[i].Print();

is said to be polymorphic because the method code performed in response to the message can take many different forms, depending on the class identity of the object.

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:

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 facilitates polymorphism.

Note?/td>

As we discussed in Chapter 6, had we chosen a different C# collection type— any type other than Array—we would not have been able to constrain the type of objects to be inserted when we declared the collection.We must therefore exercise programming discipline when inserting objects into a non-Array collection to ensure that they all speak a "common language" in terms of messages that they understand if we wish to take advantage of polymorphism. The C# compiler won't stop us from putting an assortment of objects of literally any type into a collection, but the common language runtime may complain when we try to operate on the objects after taking them back out. We'll explore this phenomenon in detail in Chapter 13 when we discuss techniques for iterating through collections.

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 < 20; i++) {
  // Process the ith student.
  // Pseudocode.
  if (studentBody[i] is an undergraduate student)
    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 < 20; 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 what specific subtype 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 PhDStudent and MastersStudent from GraduateStudent, each of which in turn overrides the Print method to provide its own "flavor" of printing as shown in Figure 7-2.

Click To expand
Figure 7-2: Subsequent overriding of Print by newly derived classes

We're 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[20];

// 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.size; 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 message by virtue of inheritance plus optional overriding.

The story is quite different, however, with the nonpolymorphic example that 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 where we're trying to sort out types of Student, and complicate our if tests even further by adding additional cases as shown here:

for (int i = 0; i < 20; 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 generic graduate student)
    studentBody[i].PrintAsGraduateStudent();
  else if ...
}

causing the "spaghetti piles" to grow ever taller.

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


Team LiB
Previous Section Next Section