12. OOP: Polymorphism, Interfaces and Operator Overloading

Objectives

In this chapter you’ll learn:

• How polymorphism enables you to “program in the general” and make systems extensible.

• To use overridden methods to effect polymorphism.

• To create abstract classes and methods.

• To determine an object’s type at execution time.

• To create sealed methods and classes.

• To declare and implement interfaces.

• To overload operators to enable them to manipulate objects.

One Ring to rule them all, One Ring to find them, One Ring to bring them all and in the darkness bind them.

John Ronald Reuel Tolkien

General propositions do not decide concrete cases.

Oliver Wendell Holmes

A philosopher of imposing stature doesn’t think in a vacuum. Even his most abstract ideas are, to some extent, conditioned by what is or is not known in the time when he lives.

Alfred North Whitehead

Outline

12.1 Introduction

12.2 Polymorphism Examples

12.3 Demonstrating Polymorphic Behavior

12.4 Abstract Classes and Methods

12.5 Case Study: Payroll System Using Polymorphism

12.5.1 Creating Abstract Base Class Employee

12.5.2 Creating Concrete Derived Class SalariedEmployee

12.5.3 Creating Concrete Derived Class HourlyEmployee

12.5.4 Creating Concrete Derived Class CommissionEmployee

12.5.5 Creating Indirect Concrete Derived Class BasePlusCommission-Employee

12.5.6 Polymorphic Processing, Operator is and Downcasting

12.5.7 Summary of the Allowed Assignments Between Base-Class and Derived-Class Variables

12.6 sealed Methods and Classes

12.7 Case Study: Creating and Using Interfaces

12.7.1 Developing an IPayable Hierarchy

12.7.2 Declaring Interface IPayable

12.7.3 Creating Class Invoice

12.7.4 Modifying Class Employee to Implement Interface IPayable

12.7.5 Modifying Class SalariedEmployee for Use with IPayable

12.7.6 Using Interface IPayable to Process Invoices and Employees Polymorphically

12.7.7 Common Interfaces of the .NET Framework Class Library

12.8 Operator Overloading

12.9 Wrap-Up

12.1 Introduction

We now continue our study of object-oriented programming by explaining and demonstrating polymorphism with inheritance hierarchies. Polymorphism enables us to “program in the general” rather than “program in the specific.” In particular, polymorphism enables us to write applications that process objects that share the same base class in a class hierarchy as if they were all objects of the base class.

Let’s consider a polymorphism example. Suppose we create an application that simulates moving several types of animals for a biological study. Classes Fish, Frog and Bird represent the types of animals under investigation. Imagine that each class extends base class Animal, which contains a method Move and maintains an animal’s current location as xyz coordinates. Each derived class implements method Move. Our application maintains an array of references to objects of the various Animal-derived classes. To simulate an animal’s movements, the application sends each object the same message once per second—namely, Move. Each specific type of Animal responds to a Move message in a unique way—a Fish might swim three feet, a Frog might jump five feet and a Bird might fly 10 feet. The application issues the Move message to each animal object generically, but each object modifies its xyz coordinates appropriately for its specific type of movement. Relying on each object to know how to “do the right thing” in response to the same method call is the key concept of polymorphism. The same message (in this case, Move) sent to a variety of objects has “many forms” of results—hence the term polymorphism.

Systems Are Easy to Extend

With polymorphism, we can design and implement systems that are easily extensible—new classes can be added with little or no modification to the general portions of the application, as long as the new classes are part of the inheritance hierarchy that the application processes generically. The only parts of an application that must be altered to accommodate new classes are those that require direct knowledge of the new classes that you add to the hierarchy. For example, if we extend class Animal to create class Tortoise (which might respond to a Move message by crawling one inch), we need to write only the Tortoise class and the part of the simulation that instantiates a Tortoise object. The portions of the simulation that process each Animal generically can remain the same.

This chapter has several parts. First, we discuss common examples of polymorphism. We then provide a live-code example demonstrating polymorphic behavior. As you’ll soon see, you’ll use base-class references to manipulate both base-class objects and derived-class objects polymorphically.

Polymorphic Employee Inheritance Hierarchy

We then present a case study that revisits the employee hierarchy of Section 11.4.5. We develop a simple payroll application that polymorphically calculates the weekly pay of several different types of employees using each employee’s Earnings method. Though the earnings of each type of employee are calculated in a specific way, polymorphism allows us to process the employees “in the general.” In the case study, we enlarge the hierarchy to include two new classes—SalariedEmployee (for people paid a fixed weekly salary) and HourlyEmployee (for people paid an hourly salary and “time-and-a-half” for overtime). We declare a common set of functionality for all the classes in the updated hierarchy in an “abstract” class, Employee, from which classes SalariedEmployee, HourlyEmployee and CommissionEmployee inherit directly and class BasePlusCommissionEmployee inherits indirectly. As you’ll soon see, when we invoke each employee’s Earnings method off a base-class Employee reference, the correct earnings calculation is performed due to C#’s polymorphic capabilities.

Determining the Type of an Object at Execution Time

Occasionally, when performing polymorphic processing, we need to program “in the specific.” Our Employee case study demonstrates that an application can determine the type of an object at execution time and act on that object accordingly. In the case study, we use these capabilities to determine whether a particular employee object is a BasePlusCommissionEmployee. If so, we increase that employee’s base salary by 10%.

Interfaces

The chapter continues with an introduction to C# interfaces. An interface describes a set of methods and properties that can be called on an object, but does not provide concrete implementations for them. You can declare classes that implement (i.e., provide concrete implementations for the methods and properties of) one or more interfaces. Each interface member must be defined for all the classes that implement the interface. Once a class implements an interface, all objects of that class have an is-a relationship with the interface type, and all objects of the class are guaranteed to provide the functionality described by the interface. This is true of all derived classes of that class as well.

Interfaces are particularly useful for assigning common functionality to possibly unrelated classes. This allows objects of unrelated classes to be processed polymorphically—objects of classes that implement the same interface can respond to the same method calls. To demonstrate creating and using interfaces, we modify our payroll application to create a general accounts-payable application that can calculate payments due for the earnings of company employees and for invoice amounts to be billed for purchased goods. As you’ll see, interfaces enable polymorphic capabilities similar to those enabled by inheritance.

Operator Overloading

This chapter ends with an introduction to operator overloading. In previous chapters, we declared our own classes and used methods to perform tasks on objects of those classes. Operator overloading allows us to define the behavior of the built-in operators, such as +, - and <, when used on objects of our own classes. This provides a much more convenient notation than calling methods for performing tasks on objects.

12.2 Polymorphism Examples

We now consider several additional examples of polymorphism.

Quadrilateral Inheritance Hierachy

If class Rectangle is derived from class Quadrilateral (a four-sided shape), then a Rectangle is a more specific version of a Quadrilateral. Any operation (e.g., calculating the perimeter or the area) that can be performed on a Quadrilateral object can also be performed on a Rectangle object. These operations also can be performed on other Quadrilaterals, such as Squares, Parallelograms and Trapezoids. The polymorphism occurs when an application invokes a method through a base-class variable—at execution time, the correct derived-class version of the method is called, based on the type of the referenced object. You’ll see a simple code example that illustrates this process in Section 12.3.

Video Game SpaceObject Inheritance Hierarchy

As another example, suppose we design a video game that manipulates objects of many different types, including objects of classes Martian, Venusian, Plutonian, SpaceShip and LaserBeam. Imagine that each class inherits from the common base class SpaceObject, which contains method Draw. Each derived class implements this method. A screen-manager application maintains a collection (e.g., a SpaceObject array) of references to objects of the various classes. To refresh the screen, the screen manager periodically sends each object the same message—namely, Draw. However, each object responds in a unique way. For example, a Martian object might draw itself in red with the appropriate number of antennae. A SpaceShip object might draw itself as a bright silver flying saucer. A LaserBeam object might draw itself as a bright red beam across the screen. Again, the same message (in this case, Draw) sent to a variety of objects has many forms of results.

A polymorphic screen manager might use polymorphism to facilitate adding new classes to a system with minimal modifications to the system’s code. Suppose we want to add Mercurian objects to our video game. To do so, we must build a Mercurian class that extends SpaceObject and provides its own Draw method implementation. When objects of class Mercurian appear in the SpaceObject collection, the screen-manager code invokes method Draw, exactly as it does for every other object in the collection, regardless of its type, so the new Mercurian objects simply “plug right in” without any modification of the screen-manager code by the programmer. Thus, without modifying the system (other than to build new classes and modify the code that creates new objects), you can use polymorphism to include additional types that might not have been envisioned when the system was created.

Software Engineering Observation 12.1

image

Polymorphism promotes extensibility: Software that invokes polymorphic behavior is independent of the object types to which messages are sent. New object types that can respond to existing method calls can be incorporated into a system without requiring modification of the base system. Only client code that instantiates new objects must be modified to accommodate new types.

12.3 Demonstrating Polymorphic Behavior

Section 11.4 created a commission-employee class hierarchy, in which class BasePlusCommissionEmployee inherited from class CommissionEmployee. The examples in that section manipulated CommissionEmployee and BasePlusCommissionEmployee objects by using references to them to invoke their methods. We aimed base-class references at base-class objects and derived-class references at derived-class objects. These assignments are natural and straightforward—base-class references are intended to refer to base-class objects, and derived-class references are intended to refer to derived-class objects. However, other assignments are possible.

In the next example, we aim a base-class reference at a derived-class object. We then show how invoking a method on a derived-class object via a base-class reference can invoke the derived-class functionality—the type of the actual referenced object, not the type of the reference, determines which method is called. This example demonstrates the key concept that an object of a derived class can be treated as an object of its base class. This enables various interesting manipulations. An application can create an array of base-class references that refer to objects of many derived-class types. This is allowed because each derived-class object is an object of its base class. For instance, we can assign the reference of a BasePlusCommissionEmployee object to a base-class CommissionEmployee variable because a BasePlusCommissionEmployee is a CommissionEmployee—so we can treat a BasePlusCommissionEmployee as a CommissionEmployee.

A base-class object is not an object of any of its derived classes. For example, we cannot directly assign the reference of a CommissionEmployee object to a derived-class BasePlusCommissionEmployee variable, because a CommissionEmployee is not a BasePlusCommissionEmployee—a CommissionEmployee does not, for example, have a baseSalary instance variable and does not have a BaseSalary property. The is-a relationship applies from a derived class to its direct and indirect base classes, but not vice versa.

The compiler allows the assignment of a base-class reference to a derived-class variable if we explicitly cast the base-class reference to the derived-class type—a technique we discuss in greater detail in Section 12.5.6. Why would we ever want to perform such an assignment? A base-class reference can be used to invoke only the methods declared in the base class—attempting to invoke derived-class-only methods through a base-class reference results in compilation errors. If an application needs to perform a derived-class-specific operation on a derived-class object referenced by a base-class variable, the application must first cast the base-class reference to a derived-class reference through a technique known as downcasting. This enables the application to invoke derived-class methods that are not in the base class. We present an example of downcasting in Section 12.5.6.

Figure 12.1 demonstrates three ways to use base-class and derived-class variables to store references to base-class and derived-class objects. The first two are straightforward—as in Section 11.4, we assign a base-class reference to a base-class variable, and we assign a derived class reference to a derived class variable. Then we demonstrate the relationship between derived classes and base classes (i.e., the is-a relationship) by assigning a derived-class reference to a base-class variable. [Note: This application uses classes CommissionEmployee and BasePlusCommissionEmployee from Fig. 11.12 and Fig. 11.13, respectively.]

Fig. 12.1. Assigning base-class and derived-class references to base-class and derived-class variables.

image

image

In Fig. 12.1, lines 11–12 create a new CommissionEmployee object and assign its reference to a CommissionEmployee variable. Lines 15–17 create a new BasePlusCommissionEmployee object and assign its reference to a BasePlusCommissionEmployee variable. These assignments are natural—for example, a CommissionEmployee variable’s primary purpose is to hold a reference to a CommissionEmployee object. Lines 21–25 use the reference commissionEmployee to invoke methods ToString and Earnings. Because commissionEmployee refers to a CommissionEmployee object, base class Commission-Employee’s version of the methods are called. Similarly, lines 29–33 use the reference basePlusCommissionEmployee to invoke the methods ToString and Earnings on the BasePlusCommissionEmployee object. This invokes derived class BasePlusCommissionEmployee’s version of the methods.

Lines 37–38 then assign the reference to derived-class object basePlusCommissionEmployee to a base-class CommissionEmployee variable, which lines 39–43 use to invoke methods ToString and Earnings. A base-class variable that contains a reference to a derived-class object and is used to call a virtual method actually calls the overriding derived-class version of the method. Hence, commissionEmployee2.ToString() in line 42 actually calls derived class BasePlusCommissionEmployee’s ToString method. The compiler allows this “crossover” because an object of a derived class is an object of its base class (but not vice versa). When the compiler encounters a method call made through a variable, the compiler determines if the method can be called by checking the variable’s class type. If that class contains the proper method declaration (or inherits one), the compiler allows the call to be compiled. At execution time, the type of the object to which the variable refers determines the actual method to use.

12.4 Abstract Classes and Methods

When we think of a class type, we assume that applications will create objects of that type. In some cases, however, it’s useful to declare classes for which you never intend to instantiate objects. Such classes are called abstract classes. Because they’re used only as base classes in inheritance hierarchies, we refer to them as abstract base classes. These classes cannot be used to instantiate objects, because, as you’ll soon see, abstract classes are incomplete—derived classes must define the “missing pieces.” We demonstrate abstract classes in Section 12.5.1.

The purpose of an abstract class is primarily to provide an appropriate base class from which other classes can inherit, and thus share a common design. In the Shape hierarchy of Fig. 11.3, for example, derived classes inherit the notion of what it means to be a Shape—common attributes such as location, color and borderThickness, and behaviors such as Draw, Move, Resize and ChangeColor. Classes that can be used to instantiate objects are called concrete classes. Such classes provide implementations of every method they declare (some of the implementations can be inherited). For example, we could derive concrete classes Circle, Square and Triangle from abstract base class TwoDimensionalShape. Similarly, we could derive concrete classes Sphere, Cube and Tetrahedron from abstract base class ThreeDimensionalShape. Abstract base classes are too general to create real objects—they specify only what is common among derived classes. We need to be more specific before we can create objects. For example, if you send the Draw message to abstract class TwoDimensionalShape, the class knows that two-dimensional shapes should be drawable, but it does not know what specific shape to draw, so it cannot implement a real Draw method. Concrete classes provide the specifics that make it reasonable to instantiate objects.

Not all inheritance hierarchies contain abstract classes. However, you’ll often write client code that uses only abstract base-class types to reduce client code’s dependencies on a range of specific derived-class types. For example, you can write a method with a parameter of an abstract base-class type. When called, such a method can be passed an object of any concrete class that directly or indirectly extends the base class specified as the parameter’s type.

Abstract classes sometimes constitute several levels of the hierarchy. For example, the Shape hierarchy of Fig. 11.3 begins with abstract class Shape. On the next level of the hierarchy are two more abstract classes, TwoDimensionalShape and ThreeDimensionalShape. The next level of the hierarchy declares concrete classes for TwoDimensionalShapes (Circle, Square and Triangle) and for ThreeDimensionalShapes (Sphere, Cube and Tetrahedron).

You make a class abstract by declaring it with the keyword abstract. An abstract class normally contains one or more abstract methods. An abstract method is one with keyword abstract in its declaration, as in

public abstract void Draw(); // abstract method

Abstract methods are implicitly virtual and do not provide implementations. A class that contains abstract methods must be declared as an abstract class even if it contains some concrete (nonabstract) methods. Each concrete derived class of an abstract base class also must provide concrete implementations of the base class’s abstract methods. We show an example of an abstract class with an abstract method in Fig. 12.4.

Properties can also be declared abstract or virtual, then overridden in derived classes with the override keyword, just like methods. This allows an abstract base class to specify common properties of its derived classes. Abstract property declarations have the form:

public abstract PropertyType MyProperty
{
   get;
   set;
} // end abstract property

The semicolons after the get and set keywords indicate that we provide no implementation for these accessors. An abstract property may omit implementations for the get accessor or the set accessor. Concrete derived classes must provide implementations for every accessor declared in the abstract property. When both get and set accessors are specified, every concrete derived class must implement both. If one accessor is omitted, the derived class is not allowed to implement that accessor. Doing so causes a compilation error.

Constructors and static methods cannot be declared abstract. Constructors are not inherited, so an abstract constructor could never be implemented. Similarly, derived classes cannot override static methods, so an abstract static method could never be implemented.

Software Engineering Observation 12.2

image

An abstract class declares common attributes and behaviors of the various classes that inherit from it, either directly or indirectly, in a class hierarchy. An abstract class typically contains one or more abstract methods or properties that concrete derived classes must override. The instance variables, concrete methods and concrete properties of an abstract class are subject to the normal rules of inheritance.

Common Programming Error 12.1

image

Attempting to instantiate an object of an abstract class is a compilation error.

Common Programming Error 12.2

image

Failure to implement a base class’s abstract methods and properties in a derived class is a compilation error unless the derived class is also declared abstract

Although we cannot instantiate objects of abstract base classes, you’ll soon see that we can use abstract base classes to declare variables that can hold references to objects of any concrete classes derived from those abstract classes. Applications typically use such variables to manipulate derived-class objects polymorphically. Also, you can use abstract base-class names to invoke static methods declared in those abstract base classes.

Polymorphism and Device Drivers

Polymorphism is particularly effective for implementing so-called layered software systems. In operating systems, for example, each type of physical device could operate quite differently from the others. Even so, common commands can read or write data from and to the devices. For each device, the operating system uses a piece of software called a device driver to control all communication between the system and the device. The write message sent to a device driver object needs to be interpreted specifically in the context of that driver and how it manipulates a specific device. However, the write call itself really is no different from the write to any other device in the system: Place some number of bytes from memory onto that device. An object-oriented operating system might use an abstract base class to provide an “interface” appropriate for all device drivers. Then, through inheritance from that abstract base class, derived classes are formed that all behave similarly. The device-driver methods are declared as abstract methods in the abstract base class. The implementations of these abstract methods are provided in the derived classes that correspond to the specific types of device drivers. New devices are always being developed, often long after the operating system has been released. When you buy a new device, it comes with a device driver provided by the device vendor. The device is immediately operational after you connect it to your computer and install the device driver. This is another elegant example of how polymorphism makes systems extensible.

Iterators

It’s common in object-oriented programming to declare an iterator class that can traverse all the objects in a collection, such as an array (Chapter 8) or a List (Chapter 9). For example, an application can print a List of objects by creating an iterator object and using it to obtain the next list element each time the iterator is called. Iterators often are used in polymorphic programming to traverse a collection that contains references to objects of various classes in an inheritance hierarchy. (Chapters 2223 present a thorough treatment of C#’s “generics” capabilities and iterators.) A List of references to objects of class TwoDimensionalShape, for example, could contain references to objects from derived classes Square, Circle, Triangle and so on. Calling method Draw for each TwoDimensionalShape object off a TwoDimensionalShape variable would polymorphically draw each object correctly on the screen.

12.5 Case Study: Payroll System Using Polymorphism

This section reexamines the CommissionEmployee-BasePlusCommissionEmployee hierarchy that we explored throughout Section 11.4. Now we use an abstract method and polymorphism to perform payroll calculations based on the type of employee. We create an enhanced employee hierarchy to solve the following problem:

A company pays its employees on a weekly basis. The employees are of four types: Salaried employees are paid a fixed weekly salary regardless of the number of hours worked, hourly employees are paid by the hour and receive “time-and-a-half” overtime pay for all hours worked in excess of 40 hours, commission employees are paid a percentage of their sales, and salaried-commission employees receive a base salary plus a percentage of their sales. For the current pay period, the company has decided to reward salaried-commission employees by adding 10% to their base salaries. The company wants to implement a C# application that performs its payroll calculations polymorphically.

We use abstract class Employee to represent the general concept of an employee. The classes that extend Employee are SalariedEmployee, CommissionEmployee and HourlyEmployee. Class BasePlusCommissionEmployee—which extends CommissionEmployee—represents the last employee type. The UML class diagram in Fig. 12.2 shows the inheritance hierarchy for our polymorphic employee payroll application. Abstract class Employee is italicized, as per the convention of the UML.

Fig. 12.2. Employee hierarchy UML class diagram.

image

Abstract base class Employee declares the “interface” to the hierarchy—that is, the set of methods that an application can invoke on all Employee objects. We use the term “interface” here in a general sense to refer to the various ways applications can communicate with objects of any Employee derived class. Be careful not to confuse the general notion of an “interface” with the formal notion of a C# interface, the subject of Section 12.7. Each employee, regardless of the way his or her earnings are calculated, has a first name, a last name and a social security number, so those pieces of data appear in abstract base class Employee.

Software Engineering Observation 12.3

image

A derived class can inherit “interface” or “implementation” from a base class. Hierarchies designed for implementation inheritance tend to have their functionality high in the hierarchy—each new derived class inherits one or more methods that were implemented in a base class, and the derived class uses the base-class implementations. Hierarchies designed for interface inheritance tend to have their functionality lower in the hierarchy—a base class specifies one or more abstract methods that must be declared for each concrete class in the hierarchy, and the individual derived classes override these methods to provide derived-class-specific implementations.

The following sections implement the Employee class hierarchy. The first section implements abstract base class Employee. The next four sections each implement one of the concrete classes. The sixth section implements a test application that builds objects of all these classes and processes those objects polymorphically.

12.5.1 Creating Abstract Base Class Employee

Class Employee (Fig. 12.4) provides methods Earnings and ToString, in addition to the auto-implemented properties that manipulate Employee’s data. An Earnings method certainly applies generically to all employees. But each earnings calculation depends on the employee’s class. So we declare Earnings as abstract in base class Employee, because a default implementation does not make sense for that method—there’s not enough information to determine what amount Earnings should return. Each derived class overrides Earnings with an appropriate implementation. To calculate an employee’s earnings, the application assigns a reference to the employee’s object to a base class Employee variable, then invokes the Earnings method on that variable. We maintain an array of Employee variables, each of which holds a reference to an Employee object (of course, there cannot be Employee objects because Employee is an abstract class—because of inheritance, however, all objects of all derived classes of Employee may nevertheless be thought of as Employee objects). The application iterates through the array and calls method Earnings for each Employee object. C# processes these method calls polymorphically. Including Earnings as an abstract method in Employee forces every directly derived concrete class of Employee to override Earnings with a method that performs an appropriate pay calculation.

Method ToString in class Employee returns a string containing the employee’s first name, last name and social security number. Each derived class of Employee overrides method ToString to create a string representation of an object of that class containing the employee’s type (e.g., "salaried employee:"), followed by the rest of the employee’s information.

The diagram in Fig. 12.3 shows each of the five classes in the hierarchy down the left side and methods Earnings and ToString across the top. For each class, the diagram shows the desired results of each method. [Note: We do not list base class Employee’s properties because they’re not overridden in any of the derived classes—each of these properties is inherited and used “as is” by each of the derived classes.]

Fig. 12.3. Polymorphic interface for the Employee hierarchy classes.

image

Let’s consider class Employee’s declaration (Fig. 12.4). The class includes a constructor that takes the first name, last name and social security number as arguments (lines 15–20); read-only properties for obtaining the first name, last name and social security number (lines 6, 9 and 12, respectively); method ToString (lines 23–27), which uses properties to return the string representation of the Employee; and abstract method Earnings (line 30), which must be implemented by concrete derived classes. The Employee constructor does not validate the social security number in this example. Normally, such validation should be provided.

Fig. 12.4. Employee abstract base class.

image

Why did we declare Earnings as an abstract method? As explained earlier, it simply does not make sense to provide an implementation of this method in class Employee. We cannot calculate the earnings for a general Employee—we first must know the specific Employee type to determine the appropriate earnings calculation. By declaring this method abstract, we indicate that each concrete derived class must provide an appropriate Earnings implementation and that an application will be able to use base-class Employee variables to invoke method Earnings polymorphically for any type of Employee.

12.5.2 Creating Concrete Derived Class SalariedEmployee

Class SalariedEmployee (Fig. 12.5) extends class Employee (line 5) and overrides Earnings (lines 34–37), which makes SalariedEmployee a concrete class. The class includes a constructor (lines 10–14) that takes a first name, a last name, a social security number and a weekly salary as arguments; property WeeklySalary (lines 17–31) to manipulate instance variable weeklySalary, including a set accessor that ensures we assign only nonnegative values to weeklySalary; method Earnings (lines 34–37) to calculate a SalariedEmployee’s earnings; and method ToString (lines 40–44), which returns a string including the employee’s type, namely, "salaried employee: ", followed by employee-specific information produced by base class Employee’s ToString method and SalariedEmployee’s WeeklySalary property. Class SalariedEmployee’s constructor passes the first name, last name and social security number to the Employee constructor (line 11) via a constructor initializer to initialize the base class’s data. Method Earnings overrides Employee’s abstract method Earnings to provide a concrete implementation that returns the SalariedEmployee’s weekly salary. If we do not implement Earnings, class SalariedEmployee must be declared abstract—otherwise, a compilation error occurs (and, of course, we want SalariedEmployee to be a concrete class).

Fig. 12.5. SalariedEmployee class that extends Employee.

image

image

SalariedEmployee method ToString (lines 40–44) overrides Employee’s version. If class SalariedEmployee did not override ToString, SalariedEmployee would have inherited the Employee version. In that case, SalariedEmployee’s ToString method would simply return the employee’s full name and social security number, which does not adequately represent a SalariedEmployee. To produce a complete string representation of a SalariedEmployee, the derived class’s ToString method returns "salaried employee: ", followed by the base-class Employee-specific information (i.e., first name, last name and social security number) obtained by invoking the base class’s ToString (line 43)—this is a nice example of code reuse. The string representation of a SalariedEmployee also contains the employee’s weekly salary, obtained by using the class’s WeeklySalary property.

12.5.3 Creating Concrete Derived Class HourlyEmployee

Class HourlyEmployee (Fig. 12.6) also extends class Employee (line 5). The class includes a constructor (lines 11–17) that takes as arguments a first name, a last name, a social security number, an hourly wage and the number of hours worked. Lines 20–34 and 37–51 declare properties Wage and Hours for instance variables wage and hours, respectively. The set accessor in property Wage ensures that wage is nonnegative, and the set accessor in property Hours ensures that hours is in the range 0168 (the total number of hours in a week) inclusive. The class overrides method Earnings (lines 54–60) to calculate an HourlyEmployee’s earnings and method ToString (lines 63–68) to return the employee’s string representation. The HourlyEmployee constructor, similarly to the SalariedEmployee constructor, passes the first name, last name and social security number to the base-class Employee constructor (line 13) to initialize the base class’s data. Also, method ToString calls base-class method ToString (line 67) to obtain the Employee-specific information (i.e., first name, last name and social security number.

Fig. 12.6. HourlyEmployee class that extends Employee.

image

image

image

12.5.4 Creating Concrete Derived Class CommissionEmployee

Class CommissionEmployee (Fig. 12.7) extends class Employee (line 5). The class includes a constructor (lines 11–16) that takes a first name, a last name, a social security number, a sales amount and a commission rate; properties (lines 19–33 and 36–50) for instance variables grossSales and commissionRate, respectively; method Earnings (lines 53–56) to calculate a CommissionEmployee’s earnings; and method ToString (lines 59–64), which returns the employee’s string representation. The CommissionEmployee’s constructor also passes the first name, last name and social security number to the Employee constructor (line 12) to initialize Employee’s data. Method ToString calls base-class method ToString (line 62) to obtain the Employee-specific information (i.e., first name, last name and social security number).

Fig. 12.7. CommissionEmployee class that extends Employee.

image

image

12.5.5 Creating Indirect Concrete Derived Class BasePlusCommissionEmployee

Class BasePlusCommissionEmployee (Fig. 12.8) extends class CommissionEmployee (line 5) and therefore is an indirect derived class of class Employee. Class BasePlusCommissionEmployee has a constructor (lines 10–15) that takes as arguments a first name, a last name, a social security number, a sales amount, a commission rate and a base salary. It then passes the first name, last name, social security number, sales amount and commission rate to the CommissionEmployee constructor (line 12) to initialize the base class’s data. BasePlusCommissionEmployee also contains property BaseSalary (lines 19–33) to manipulate instance variable baseSalary. Method Earnings (lines 36–39) calculates a BasePlusCommissionEmployee’s earnings. Line 38 in method Earnings calls base class CommissionEmployee’s Earnings method to calculate the commission-based portion of the employee’s earnings. Again, this shows the benefits of code reuse. BasePlusCommissionEmployee’s ToString method (lines 42–46) creates a string representation of a BasePlusCommissionEmployee that contains "base-salaried", followed by the string obtained by invoking base class CommissionEmployee’s ToString method (another example of code reuse), then the base salary. The result is a string beginning with "base-salaried commission employee", followed by the rest of the BasePlusCommissionEmployee’s information. Recall that CommissionEmployee’s ToString method obtains the employee’s first name, last name and social security number by invoking the ToString method of its base class (i.e., Employee)—a further demonstration of code reuse. BasePlusCommissionEmployee’s ToString initiates a chain of method calls that spans all three levels of the Employee hierarchy.

Fig. 12.8. BasePlusCommissionEmployee class that extends CommissionEmployee.

image

image

12.5.6 Polymorphic Processing, Operator is and Downcasting

To test our Employee hierarchy, the application in Fig. 12.9 creates an object of each of the four concrete classes SalariedEmployee, HourlyEmployee, CommissionEmployee and BasePlusCommissionEmployee. The application manipulates these objects, first via variables of each object’s own type, then polymorphically, using an array of Employee variables. While processing the objects polymorphically, the application increases the base salary of each BasePlusCommissionEmployee by 10% (this, of course, requires determining the object’s type at execution time). Finally, the application polymorphically determines and outputs the type of each object in the Employee array. Lines 10–20 create objects of each of the four concrete Employee derived classes. Lines 24–32 output the string representation and earnings of each of these objects. Each object’s ToString method is called implicitly by WriteLine when the object is output as a string with format items.

Fig. 12.9. Employee hierarchy test application.

image

image

image

image

Assigning Derived-Class Objects to Base-Class References

Line 35 declares employees and assigns it an array of four Employee variables. Lines 38–41 assign a SalariedEmployee object, an HourlyEmployee object, a CommissionEmployee object and a BasePlusCommissionEmployee object to employees[0], employees[1], employees[2] and employees[3], respectively. Each assignment is allowed, because a SalariedEmployee is an Employee, an HourlyEmployee is an Employee, a CommissionEmployee is an Employee and a BasePlusCommissionEmployee is an Employee. Therefore, we can assign the references of SalariedEmployee, HourlyEmployee, CommissionEmployee and BasePlusCommissionEmployee objects to base-class Employee variables, even though Employee is an abstract class.

Polymorphically Processing Employees

Lines 46–66 iterate through array employees and invoke methods ToString and Earnings with Employee variable currentEmployee, which is assigned the reference to a different Employee during each iteration. The output illustrates that the appropriate methods for each class are indeed invoked. All calls to virtual methods ToString and Earnings are resolved at execution time, based on the type of the object to which currentEmployee refers. This process is known as dynamic binding or late binding. For example, line 48 implicitly invokes method ToString of the object to which currentEmployee refers. Only the methods of class Employee can be called via an Employee variable—and Employee includes class object’s methods, such as ToString. (Section 11.7 discussed the methods that all classes inherit from class object.) A base-class reference can be used to invoke only methods of the base class.

Giving BasePlusCommissionEmployees 10% Raises

We perform special processing on BasePlusCommissionEmployee objects—as we encounter them, we increase their base salary by 10%. When processing objects polymorphically, we typically do not need to worry about the “specifics,” but to adjust the base salary, we do have to determine the specific type of each Employee object at execution time. Line 51 uses the is operator to determine whether a particular Employee object’s type is BasePlusCommissionEmployee. The condition in line 51 is true if the object referenced by currentEmployee is a BasePlusCommissionEmployee. This would also be true for any object of a BasePlusCommissionEmployee derived class (if there were any), because of the isa relationship a derived class has with its base class. Lines 55–56 downcast current-Employee from type Employee to type BasePlusCommissionEmployee—this cast is allowed only if the object has an is-a relationship with BasePlusCommissionEmployee. The condition at line 51 ensures that this is the case. This cast is required if we are to use derived class BasePlusCommissionEmployee’s BaseSalary property on the current Employee object—attempting to invoke a derived-class-only method directly on a base class reference is a compilation error.

Common Programming Error 12.3

image

Assigning a base-class variable to a derived-class variable (without an explicit downcast) is a compilation error.

Software Engineering Observation 12.4

image

If at execution time the reference to a derived-class object has been assigned to a variable of one of its direct or indirect base classes, it’s acceptable to cast the reference stored in that base-class variable back to a reference of the derived-class type. Before performing such a cast, use the is operator to ensure that the object is indeed an object of an appropriate derived-class type.

When downcasting an object, an InvalidCastException (of namespace System) occurs if at execution time the object does not have an is a relationship with the type specified in the cast operator. An object can be cast only to its own type or to the type of one of its base classes. You can avoid a potential InvalidCastException by using the as operator to perform a downcast rather than a cast operator. For example, in the statement

BasePlusCommissionEmployee employee =
   currentEmployee as BasePlusCommissionEmployee;

employee is assigned a reference to an object that is a BasePlusCommissionEmployee, or the value null if currentEmployee is not a BasePlusCommissionEmployee. You can then compare employee with null to determine whether the cast succeeded.

If the is expression in line 51 is true, the if statement (lines 51–62) performs the special processing required for the BasePlusCommissionEmployee object. Using BasePlusCommissionEmployee variable employee, line 58 accesses the derived-class-only property BaseSalary to retrieve and update the employee’s base salary with the 10% raise.

Lines 64–65 invoke method Earnings on currentEmployee, which calls the appropriate derived-class object’s Earnings method polymorphically. Obtaining the earnings of the SalariedEmployee, HourlyEmployee and CommissionEmployee polymorphically in lines 64–65 produces the same result as obtaining these employees’ earnings individually in lines 24–29. However, the earnings amount obtained for the BasePlusCommissionEmployee in lines 64–65 is higher than that obtained in lines 30–32, due to the 10% increase in its base salary.

Every Object Knows Its Own Type

Lines 69–71 display each employee’s type as a string. Every object in C# knows its own type and can access this information through method GetType, which all classes inherit from class object. Method GetType returns an object of class Type (of namespace System), which contains information about the object’s type, including its class name, the names of its methods, and the name of its base class. Line 71 invokes method GetType on the object to get its runtime class (i.e., a Type object that represents the object’s type). Then method ToString is implicitly invoked on the object returned by GetType. The Type class’s ToString method returns the class name.

Avoiding Compilation Errors with Downcasting

In the previous example, we avoid several compilation errors by downcasting an Employee variable to a BasePlusCommissionEmployee variable in lines 55–56. If we remove the cast operator (BasePlusCommissionEmployee) from line 56 and attempt to assign Employee variable currentEmployee directly to BasePlusCommissionEmployee variable employee, we receive a “Cannot implicitly convert type” compilation error. This error indicates that the attempt to assign the reference of base-class object commissionEmployee to derived-class variable basePlusCommissionEmployee is not allowed without an appropriate cast operator. The compiler prevents this assignment, because a CommissionEmployee is not a BasePlusCommissionEmployee—again, the is-a relationship applies only between the derived class and its base classes, not vice versa.

Similarly, if lines 58 and 61 use base-class variable currentEmployee, rather than derived-class variable employee, to use derived-class-only property BaseSalary, we receive an “'Employee' does not contain a definition for 'BaseSalary'” compilation error on each of these lines. Attempting to invoke derived-class-only methods on a base-class reference is not allowed. While lines 58 and 61 execute only if is in line 51 returns true to indicate that currentEmployee has been assigned a reference to a BasePlusCommissionEmployee object, we cannot attempt to use derived-class BasePlusCommissionEmployee property BaseSalary with base-class Employee reference currentEmployee. The compiler would generate errors in lines 58 and 61, because BaseSalary is not a base-class member and cannot be used with a base-class variable. Although the actual method that’s called depends on the object’s type at execution time, a variable can be used to invoke only those methods that are members of that variable’s type, which the compiler verifies. Using a base-class Employee variable, we can invoke only methods and properties found in class Employee—methods Earnings and ToString, and properties FirstName, LastName and SocialSecurityNumber—and method methods inherited from class object.

12.5.7 Summary of the Allowed Assignments Between Base-Class and Derived-Class Variables

Now that you’ve seen a complete application that processes diverse derived-class objects polymorphically, we summarize what you can and cannot do with base-class and derived-class objects and variables. Although a derived-class object also is a base-class object, the two are nevertheless different. As discussed previously, derived-class objects can be treated as if they were base-class objects. However, the derived class can have additional derived-class-only members. For this reason, assigning a base-class reference to a derived-class variable is not allowed without an explicit cast—such an assignment would leave the derived-class members undefined for a base-class object.

We’ve discussed four ways to assign base-class and derived-class references to variables of base-class and derived-class types:

  1. Assigning a base-class reference to a base-class variable is straightforward.
  2. Assigning a derived-class reference to a derived-class variable is straightforward.
  3. Assigning a derived-class reference to a base-class variable is safe, because the derived-class object is an object of its base class. However, this reference can be used to refer only to base-class members. If this code refers to derived-class-only members through the base-class variable, the compiler reports errors.
  4. Attempting to assign a base-class reference to a derived-class variable is a compilation error. To avoid this error, the base-class reference must be cast to a derived-class type explicitly or must be converted using the as operator. At execution time, if the object to which the reference refers is not a derived-class object, an exception will occur. The is operator can be used to ensure that such a cast is performed only if the object is a derived-class object.

12.6 sealed Methods and Classes

Only methods declared virtual, override or abstract can be overridden in derived classes. A method declared sealed in a base class cannot be overridden in a derived class. Methods that are declared private are implicitly sealed, because it’s impossible to override them in a derived class (though the derived class can declare a new method with the same signature as the private method in the base class). Methods that are declared static also are implicitly sealed, because static methods cannot be overridden either. A derived-class method declared both override and sealed can override a base-class method, but cannot be overridden in derived classes further down the inheritance hierarchy.

A sealed method’s declaration can never change, so all derived classes use the same method implementation, and calls to sealed methods are resolved at compile time—this is known as static binding. Since the compiler knows that sealed methods cannot be overridden, it can often optimize code by removing calls to sealed methods and replacing them with the expanded code of their declarations at each method-call location—a technique known as inlining the code.

Performance Tip 12.1

image

The compiler can decide to inline a sealed method call and will do so for small, simple sealed methods. Inlining does not violate encapsulation or information hiding, but does improve performance, because it eliminates the overhead of making a method call.

A class that’s declared sealed cannot be a base class (i.e., a class cannot extend a sealed class). All methods in a sealed class are implicitly sealed. Class string is a sealed class. This class cannot be extended, so applications that use strings can rely on the functionality of string objects as specified in the Framework Class Library.

Common Programming Error 12.4

image

Attempting to declare a derived class of a sealed class is a compilation error.

12.7 Case Study: Creating and Using Interfaces

Our next example (Figs. 12.1112.15) reexamines the payroll system of Section 12.5. Suppose that the company involved wishes to perform several accounting operations in a single accounts-payable application—in addition to calculating the payroll earnings that must be paid to each employee, the company must also calculate the payment due on each of several invoices (i.e., bills for goods purchased). Though applied to unrelated things (i.e., employees and invoices), both operations have to do with calculating some kind of payment amount. For an employee, the payment refers to the employee’s earnings. For an invoice, the payment refers to the total cost of the goods listed on the invoice. Can we calculate such different things as the payments due for employees and invoices polymorphically in a single application? Does C# offer a capability that requires that unrelated classes implement a set of common methods (e.g., a method that calculates a payment amount)? C# interfaces offer exactly this capability.

Interfaces define and standardize the ways in which people and systems can interact with one another. For example, the controls on a radio serve as an interface between a radio’s users and its internal components. The controls allow users to perform a limited set of operations (e.g., changing the station, adjusting the volume, choosing between AM and FM), and different radios may implement the controls in different ways (e.g., using push buttons, dials, voice commands). The interface specifies what operations a radio must permit users to perform but does not specify how they’re performed. Similarly, the interface between a driver and a car with a manual transmission includes the steering wheel, the gear shift, the clutch pedal, the gas pedal and the brake pedal. This same interface is found in nearly all manual-transmission cars, enabling someone who knows how to drive one particular manual-transmission car to drive just about any other. The components of each car may look a bit different, but the general purpose is the same—to allow people to drive the car.

Software objects also communicate via interfaces. A C# interface describes a set of methods and properties that can be called on an object—to tell it, for example, to perform some task or return some piece of information. The next example introduces an interface named IPayable that describes the functionality of any object that must be capable of being paid and thus must offer a method to determine the proper payment amount due. An interface declaration begins with the keyword interface and can contain only abstract methods, properties, indexers and events (events are discussed in Chapter 14, Graphical User Interfaces with Windows Forms: Part 1.) All interface members are implicitly declared both public and abstract. In addition, each interface can extend one or more other interfaces to create a more elaborate interface that other classes can implement.

Common Programming Error 12.5

image

It’s a compilation error to declare an interface member public or abstract explicitly, because they’re redundant in interface-member declarations. It’s also a compilation error to specify any implementation details, such as concrete method declarations, in an interface.

To use an interface, a class must specify that it implements the interface by listing the interface after the colon (:) in the class declaration. This is the same syntax used to indicate inheritance from a base class. A concrete class implementing the interface must declare each member of the interface with the signature specified in the interface declaration. A class that implements an interface but does not implement all its members is an abstract class—it must be declared abstract and must contain an abstract declaration for each unimplemented member of the interface. Implementing an interface is like signing a contract with the compiler that states, “I will provide an implementation for all the members specified by the interface, or I will declare them abstract.”

Common Programming Error 12.6

image

Failing to define or declare any member of an interface in a class that implements the interface results in a compilation error.

An interface is typically used when unrelated classes need to share common methods. This allows objects of unrelated classes to be processed polymorphically—objects of classes that implement the same interface can respond to the same method calls. You can create an interface that describes the desired functionality, then implement this interface in any classes requiring that functionality. For example, in the accounts-payable application developed in this section, we implement interface IPayable in any class that must be able to calculate a payment amount (e.g., Employee, Invoice).

An interface often is used in place of an abstract class when there’s no default implementation to inherit—that is, no fields and no default method implementations. Like abstract classes, interfaces are typically public types, so they’re normally declared in files by themselves with the same name as the interface and the .cs file-name extension.

12.7.1 Developing an IPayable Hierarchy

To build an application that can determine payments for employees and invoices alike, we first create an interface named IPayable. Interface IPayable contains method GetPaymentAmount that returns a decimal amount to be paid for an object of any class that implements the interface. Method GetPaymentAmount is a general-purpose version of method Earnings of the Employee hierarchy—method Earnings calculates a payment amount specifically for an Employee, while GetPaymentAmount can be applied to a broad range of unrelated objects. After declaring interface IPayable, we introduce class Invoice, which implements interface IPayable. We then modify class Employee such that it also implements interface IPayable. Finally, we update Employee derived class SalariedEmployee to “fit” into the IPayable hierarchy (i.e., we rename SalariedEmployee method Earnings as GetPaymentAmount).

Good Programming Practice 12.1

image

By convention, the name of an interface begins with “I”. This helps distinguish interfaces from classes, improving code readability.

Good Programming Practice 12.2

image

When declaring a method in an interface, choose a name that describes the method’s purpose in a general manner, because the method may be implemented by a broad range of unrelated classes.

Classes Invoice and Employee both represent things for which the company must be able to calculate a payment amount. Both classes implement IPayable, so an application can invoke method GetPaymentAmount on Invoice objects and Employee objects alike. This enables the polymorphic processing of Invoices and Employees required for our company’s accounts-payable application.

The UML class diagram in Fig. 12.10 shows the interface and class hierarchy used in our accounts-payable application. The hierarchy begins with interface IPayable. The UML distinguishes an interface from a class by placing the word “interface” in guillemets (« and ») above the interface name. The UML expresses the relationship between a class and an interface through a realization. A class is said to “realize,” or implement, an interface. A class diagram models a realization as a dashed arrow with a hollow arrowhead pointing from the implementing class to the interface. The diagram in Fig. 12.10 indicates that classes Invoice and Employee each realize (i.e., implement) interface IPayable. As in the class diagram of Fig. 12.2, class Employee appears in italics, indicating that it’s an abstract class. Concrete class SalariedEmployee extends Employee and inherits its base class’s realization relationship with interface IPayable.

Fig. 12.10. IPayable interface and class hierarchy UML class diagram.

image

12.7.2 Declaring Interface IPayable

The declaration of interface IPayable begins in Fig. 12.11 at line 3. Interface IPayable contains public abstract method GetPaymentAmount (line 5). The method cannot be explicitly declared public or abstract. Interfaces can have any number of members and interface methods can have parameters.

Fig. 12.11. IPayable interface declaration.

image

12.7.3 Creating Class Invoice

We now create class Invoice (Fig. 12.12) to represent a simple invoice that contains billing information for one kind of part. The class contains properties PartNumber (line 11), PartDescription (line 14), Quantity (lines 27–41) and PricePerItem (lines 44–58) that indicate the part number, the description of the part, the quantity of the part ordered and the price per item. Class Invoice also contains a constructor (lines 17–24) and a ToString method (lines 61–67) that returns a string representation of an Invoice object. The set accessors of properties Quantity and PricePerItem ensure that quantity and pricePerItem are assigned only nonnegative values.

Fig. 12.12. Invoice class implements IPayable.

image

image

image

Line 5 of Fig. 12.12 indicates that class Invoice implements interface IPayable. Like all classes, class Invoice also implicitly inherits from class object. C# does not allow derived classes to inherit from more than one base class, but it does allow a class to inherit from a base class and implement any number of interfaces. All objects of a class that implement multiple interfaces have the is-a relationship with each implemented interface type. To implement more than one interface, use a comma-separated list of interface names after the colon (:) in the class declaration, as in:

public class ClassName : BaseClassName, FirstInterface, SecondInterface, ...

When a class inherits from a base class and implements one or more interfaces, the class declaration must list the base-class name before any interface names.

Class Invoice implements the one method in interface IPayable—method GetPaymentAmount is declared in lines 70–73. The method calculates the amount required to pay the invoice. The method multiplies the values of quantity and pricePerItem (obtained through the appropriate properties) and returns the result (line 72). This method satisfies the implementation requirement for the method in interface IPayable—we’ve fulfilled the interface contract with the compiler.

12.7.4 Modifying Class Employee to Implement Interface IPayable

We now modify class Employee to implement interface IPayable. Figure 12.13 contains the modified Employee class. This class declaration is identical to that of Fig. 12.4 with two exceptions. First, line 3 of Fig. 12.13 indicates that class Employee now implements interface IPayable. Because of this, we must rename Earnings to GetPaymentAmount throughout the Employee hierarchy. As with method Earnings in the version of class Employee in Fig. 12.4, however, it does not make sense to implement method GetPaymentAmount in class Employee, because we cannot calculate the earnings payment owed to a general Employee—first, we must know the specific type of Employee. In Fig. 12.4, we declared method Earnings as abstract for this reason, and as a result, class Employee had to be declared abstract. This forced each Employee derived class to override Earnings with a concrete implementation.

Fig. 12.13. Employee abstract base class.

image

In Fig. 12.13, we handle this situation the same way. Recall that when a class implements an interface, the class makes a contract with the compiler stating that the class either will implement each of the methods in the interface or will declare them abstract. If the latter option is chosen, we must also declare the class abstract. As we discussed in Section 12.4, any concrete derived class of the abstract class must implement the abstract methods of the base class. If the derived class does not do so, it too must be declared abstract. As indicated by the comments in lines 29–30, class Employee of Fig. 12.13 does not implement method GetPaymentAmount, so the class is declared abstract.

12.7.5 Modifying Class SalariedEmployee for Use with IPayable

Figure 12.14 contains a modified version of class SalariedEmployee that extends Employee and implements method GetPaymentAmount. This version of SalariedEmployee is identical to that of Fig. 12.5 with the exception that the version here implements method GetPaymentAmount (lines 35–38) instead of method Earnings. The two methods contain the same functionality but have different names. Recall that the IPayable version of the method has a more general name to be applicable to possibly disparate classes. The remaining Employee derived classes (e.g., HourlyEmployee, CommissionEmployee and BasePlusCommissionEmployee) also must be modified to contain method GetPaymentAmount in place of Earnings to reflect the fact that Employee now implements IPayable. We leave these modifications to try on your own and use only SalariedEmployee in our test application in this section.

Fig. 12.14. SalariedEmployee class that extends Employee.

image

image

When a class implements an interface, the same is-a relationship provided by inheritance applies. Class Employee implements IPayable, so we can say that an Employee is an IPayable, as are any classes that extend Employee. As such, SalariedEmployee objects are IPayable objects. An object of a class that implements an interface may be thought of as an object of the interface type. Objects of any classes derived from the class that implements the interface can also be thought of as objects of the interface type. Thus, just as we can assign the reference of a SalariedEmployee object to a base-class Employee variable, we can assign the reference of a SalariedEmployee object to an interface IPayable variable. Invoice implements IPayable, so an Invoice object also is an IPayable object, and we can assign the reference of an Invoice object to an IPayable variable.

Software Engineering Observation 12.5

image

Inheritance and interfaces are similar in their implementation of the is-a relationship. An object of a class that implements an interface may be thought of as an object of that interface type. An object of any derived classes of a class that implements an interface also can be thought of as an object of the interface type.

Software Engineering Observation 12.6

image

The is-a relationship that exists between base classes and derived classes, and between interfaces and the classes that implement them, holds when passing an object to a method. When a method parameter receives an argument of a base class or interface type, the method polymorphically processes the object received as an argument.

12.7.6 Using Interface IPayable to Process Invoices and Employees Polymorphically

PayableInterfaceTest (Fig. 12.15) illustrates that interface IPayable can be used to process a set of Invoices and Employees polymorphically in a single application. Line 10 declares payableObjects and assigns it an array of four IPayable variables. Lines 13–14 assign the references of Invoice objects to the first two elements of payableObjects. Lines 15–18 assign the references of SalariedEmployee objects to the remaining two elements of payableObjects. These assignments are allowed because an Invoice is an IPayable, a SalariedEmployee is an Employee and an Employee is an IPayable. Lines 24–29 use a foreach statement to process each IPayable object in payableObjects polymorphically, printing the object as a string, along with the payment due. Lines 27–28 implicitly invokes method ToString off an IPayable interface reference, even though ToString is not declared in interface IPayable—all references (including those of interface types) refer to objects that extend object and therefore have a ToString method. Line 28 invokes IPayable method GetPaymentAmount to obtain the payment amount for each object in payableObjects, regardless of the actual type of the object. The output reveals that the method calls in lines 27–28 invoke the appropriate class’s implementation of methods ToString and GetPaymentAmount. For instance, when currentPayable refers to an Invoice during the first iteration of the foreach loop, class Invoice’s ToString and GetPaymentAmount methods execute.

Software Engineering Observation 12.7

image

All methods of class object can be called by using a reference of an interface type—the reference refers to an object, and all objects inherit the methods of class object.

Fig. 12.15. Tests interface IPayable with disparate classes.

image

image

12.7.7 Common Interfaces of the .NET Framework Class Library

In this section, we overview several common interfaces defined in the .NET Framework Class Library. These interfaces are implemented and used in the same manner as those you create (e.g., interface IPayable in Section 12.7.2). The Framework Class Library’s interfaces enable you to extend many important aspects of C# with your own classes. Figure 12.16 overviews several commonly used Framework Class Library interfaces.

Fig. 12.16. Common interfaces of the .NET Framework Class Library.

image

image

12.8 Operator Overloading

Object manipulations are accomplished by sending messages (in the form of method calls) to the objects. This method-call notation is cumbersome for certain kinds of classes, especially mathematical classes. For these classes, it would be convenient to use C#’s rich set of built-in operators to specify object manipulations. In this section, we show how to enable these operators to work with class objects—via a process called operator overloading.

C# enables you to overload most operators to make them sensitive to the context in which they’re used. Some operators are overloaded more frequently than others, especially the various arithmetic operators, such as + and -, where operator notation often is more natural. Figures 12.17 and 12.18 provide an example of using operator overloading with a ComplexNumber class. For a list of overloadable operators, see msdn.microsoft.com/en-us/library/8edha89s.aspx.

Fig. 12.17. Class that overloads operators for adding, subtracting and multiplying complex numbers.

image

image

Fig. 12.18. Overloading operators for complex numbers.

image

image

Class ComplexNumber (Fig. 12.17) overloads the plus (+), minus (-) and multiplication (*) operators to enable programs to add, subtract and multiply instances of class ComplexNumber using common mathematical notation. Lines 9 and 12 define properties for the Real and Imaginary components of the complex number.

Lines 29–34 overload the plus operator (+) to perform addition of ComplexNumbers. Keyword operator, followed by an operator symbol, indicates that a method overloads the specified operator. Methods that overload binary operators must take two arguments. The first argument is the left operand, and the second argument is the right operand. Class ComplexNumber’s overloaded plus operator takes two ComplexNumber references as arguments and returns a ComplexNumber that represents the sum of the arguments. This method is marked public and static, which is required for overloaded operators. The body of the method (lines 32–33) performs the addition and returns the result as a new ComplexNumber. Notice that we do not modify the contents of either of the original operands passed as arguments x and y. This matches our intuitive sense of how this operator should behave—adding two numbers does not modify either of the original numbers. Lines 37–51 provide similar overloaded operators for subtracting and multiplying ComplexNumbers.

Software Engineering Observation 12.8

image

Overload operators to perform the same function or similar functions on class objects as the operators perform on objects of simple types. Avoid nonintuitive use of operators.

Software Engineering Observation 12.9

image

At least one parameter of an overloaded operator method must be a reference to an object of the class in which the operator is overloaded. This prevents you from changing how operators work on simple types.

Class ComplexTest (Fig. 12.18) demonstrates the overloaded ComplexNumber operators +, - and *. Lines 14–27 prompt the user to enter two complex numbers, then use this input to create two ComplexNumbers and assign them to variables x and y.

Lines 31–33 add, subtract and multiply x and y with the overloaded operators, then output the results. In line 31, we perform the addition by using the plus operator with ComplexNumber operands x and y. Without operator overloading, the expression x + y wouldn’t make sense—the compiler wouldn’t know how two objects of class Complex-Number should be added. This expression makes sense here because we’ve defined the plus operator for two ComplexNumbers in lines 29–34 of Fig. 12.17. When the two Complex-Numbers are “added” in line 31 of Fig. 12.18, this invokes the operator+ declaration, passing the left operand as the first argument and the right operand as the second argument. When we use the subtraction and multiplication operators in lines 32–33, their respective overloaded operator declarations are invoked similarly.

Each calculation’s result is a reference to a new ComplexNumber object. When this new object is passed to the Console class’s WriteLine method, its ToString method (Fig. 12.17, lines 22–26) is implicitly invoked. Line 31 of Fig. 12.18 could be rewritten to explicitly invoke the ToString method of the object created by the overloaded plus operator, as in:

Console.WriteLine( "{0} + {1} = {2}", x, y, ( x + y ).ToString() );

12.9 Wrap-Up

This chapter introduced polymorphism—the ability to process objects that share the same base class in a class hierarchy as if they were all objects of the base class. The chapter discussed how polymorphism makes systems extensible and maintainable, then demonstrated how to use overridden methods to effect polymorphic behavior. We introduced the notion of an abstract class, which allows you to provide an appropriate base class from which other classes can inherit. You learned that an abstract class can declare abstract methods that each derived class must implement to become a concrete class, and that an application can use variables of an abstract class to invoke derived class implementations of abstract methods polymorphically. You also learned how to determine an object’s type at execution time. We showed how to create sealed methods and classes. The chapter discussed declaring and implementing an interface as another way to achieve polymorphic behavior, often among objects of different classes. Finally, you learned how to define the behavior of the built-in operators on objects of your own classes with operator overloading.

You should now be familiar with classes, objects, encapsulation, inheritance, interfaces and polymorphism—the most essential aspects of object-oriented programming. Next, we take a deeper look at using exception handling to deal with runtime errors.

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

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