Let's say you write a method that needs to iterate through a collection of Employee objects, calling each object's CalculatePay method. That works fine when your company has one employee type because you can then insert the exact object type into the collection. However, what happens when you start hiring other employee types? For example, if you have a class called Employee and it implements the functionality of a salaried employee, what do you do when you start hiring contract employees whose salaries have to be computed differently? Well, in a procedural language, you would modify the function to handle the new employee type, since old code can't possibly know how to handle new code. An object-oriented solution handles differences like this through polymorphism.-
Using our example, you would define a base class called Employee. You then define a derived class for each employee type (as we've seen previously). Each derived employee class would then have its own implementation of the CalculatePay method. Here's where the magic occurs. With polymorphism, when you have an upcasted pointer to an object and you call that object's method, the language's runtime will ensure that the correct version of the method is called. Here's the code to illustrate what I'm talking about: -
using System; class Employee { public Employee(string firstName, string lastName, int age, double payRate) { this.firstName = firstName; this.lastName = lastName; this.age = age; this.payRate = payRate; } protected string firstName; protected string lastName; protected int age; protected double payRate; public virtual double CalculatePay(int hoursWorked) { Console.WriteLine("Employee.CalculatePay"); return 42; // bogus value } } class SalariedEmployee : Employee { public SalariedEmployee(string firstName, string lastName, int age, double payRate) : base(firstName, lastName, age, payRate) {} public override double CalculatePay(int hoursWorked) { Console.WriteLine("SalariedEmployee.CalculatePay"); return 42; // bogus value } } class ContractorEmployee : Employee { public ContractorEmployee(string firstName, string lastName, int age, double payRate) : base(firstName, lastName, age, payRate) {} public override double CalculatePay(int hoursWorked) { Console.WriteLine("ContractorEmployee.CalculatePay"); return 42; // bogus value } } class HourlyEmployee : Employee { public HourlyEmployee(string firstName, string lastName, int age, double payRate) : base(firstName, lastName, age, payRate) {} public override double CalculatePay(int hoursWorked) { Console.WriteLine("HourlyEmployee.CalculatePay"); return 42; // bogus value } } class PolyApp { protected Employee[] employees; protected void LoadEmployees() { Console.WriteLine("Loading employees..."); // In a real application, we'd probably get this // from a database. employees = new Employee[3]; employees[0] = new SalariedEmployee ("Amy", "Anderson", 28, 100); employees[1] = new ContractorEmployee ("John", "Maffei", 35, 110); employees[2] = new HourlyEmployee ("Lani", "Ota", 2000, 5); Console.WriteLine("\n"); } protected void CalculatePay() { foreach(Employee emp in employees) { emp.CalculatePay(40); } } public static void Main() { PolyApp app = new PolyApp(); app.LoadEmployees(); app.CalculatePay(); } }
Compiling and running this application will yield the following results: -
c:\>PolyApp Loading employees... SalariedEmployee.CalculatePay ContractorEmployee.CalculatePay HourlyEmployee.CalculatePay
Note that polymorphism provides at least two benefits. First, it gives you the ability to group objects that have a common base class and treat them consistently. In the example above, although technically I have three different object types-SalariedEmployee, ContractorEmployee,and HourlyEmployee-I can treat them all as Employee objects because they all derive from the Employee base class. This is how I can stuff them in an array that is defined as an array of Employee objects. Because of polymorphism, when I call one of those object's methods, the runtime will ensure that the correct derived object's method is called.
The second advantage is the one I mentioned at the beginning of this section: old code can use new code. Notice that the PolyApp.CalculatePay method iterates through its member array of Employee objects. Because this method extracts the objects as implicitly upcasted Employee objects and the runtime's implementation of polymorphism ensures that the correct derived class's method is called, I can add other derived employee types to the system, insert them into the Employee object array, and all my code continues working without me having to change any of my original code! -
Summary
This chapter has taken you on a whirlwind tour through terminology and concepts that fall under the umbrella of object-oriented programming. A full discourse on the subject would require several chapters and would. A firm grasp of object-oriented fundamentals is imperative to getting the most out of the C# language.
We covered quite a few ideas in this chapter. Key to understanding object-oriented systems is knowing the difference between classes, objects, and interfaces and how these concepts relate to effective solutions. Good object-oriented solutions also depend on a sound implementation of the three tenets of object-oriented programming: encapsulation, inheritance, and polymorphism. The concepts presented in this chapter lay the groundwork for the next chapter, which introduces the Microsoft .NET Framework and the Common Language Runtime.